CentOS 7でキーボードを日本語配列にできず記号入力等で困る事象の対処
■概要
仮想環境上のCentOS7のキー入力が日本語配列にならず、記号('@'や'='等)の入力が面倒なことになっていたので、その対処に本腰を入れた時の記録。
最終的には日本語配列に変更できたが、釈然としない結果に。
理屈はよく分からないがとりあえず直ったので備忘。
環境:
MacOS Monterey 12.2.1
VMware Fusion 12.2.3
CentOS 7.9
物理キーボード:iMac付属のJIS配列日本語キーボード
■結論
「地域と言語」の設定で、入力ソースに「日本語(かな漢字)」だけでなく、「日本語」も追加する。
■背景説明
Mac上で動かしている VMware Fusion 上に、CentOS の仮想マシンを動かしている。
キーボードはiMac付属の日本語配列のものを使用。
CentOSの言語やキーボード設定をしてもなぜか入力が英語配列のままになり、特にコマンドで記号を打つ場合に困っていた。(「:」はShift+「;」、「=」はShitf+「^」など)
数年前にも対処に乗り出したが結局分からず退散、今回二度目の挑戦。
解決前の設定状況
解決前の時点では下記の設定をしていた。
ただ下記の設定が問題解決のために必要な設定だったのかは不明。
#そもそも下記のように locale や keymap をjp106 にしても
#この事象が起きている場合、下記がus設定であろうと
#本記事で記載している対処をすれば日本語配列にできると思われる。
# localectl status System Locale: LANG=ja_JP.utf8 VC Keymap: jp106 X11 Layout: jp X11 Model: jp106 X11 Options: terminate:ctrl_alt_bksp # cat /etc/vconsole.conf KEYMAP=jp106 FONT=latarcyrheb-sun16
なお、日本語入力自体はできている状態。
あまり関係ないがmacキーボードの「英数」キー、「かな」キーの操作は、VMwareのキーマップ設定 & CentOSの「日本語(かな漢字)」のショートカット設定によって、直接入⇄かな の入力変換をできるようにしている。
対処方法
試行錯誤の末、入力ソースに「日本語」を追加すれば日本語配列になると分かった。
元々入力ソースは「日本語(かな漢字)」のみを設定していた。
そこに「日本語」も追加する。
まだこの時点では、「日本語(かな漢字)」は英語配列のはず(キーボードアイコンからキーボード配列を確認すると英語配列になっている)。
一方「日本語」は日本語配列になっている。
そこで入力を、先ほど追加した「日本語」に設定する(デスクトップ上部メニューの曜日の左にある入力設定から変更)。
すると何故かその時点で「日本語(かな漢字)」も日本語配列になる(キーボードアイコンからキーボード配列を確認しても日本語配列に変わっている)。
そうなれば入力ソースの「日本語」を削除しても「日本語(かな漢字)」は日本語配列のまま使える。
ただし「日本語」を削除して「日本語(かな漢字)」だけの状態に戻すと、OS再起動時にまた英語配列になってしまう。
「日本語」を削除せずに残しておくと、OS再起動後も「日本語(かな漢字)」が日本語配列のまま使える。
この辺りの理屈がいまいちよく分からず、釈然としない...。
※先ほど追加した入力ソースの「日本語」は元から日本語配列のようなのでそもそもこちらを使ってもいいかもしれないが、ショートカットが設定できなかったので「日本語(かな漢字)」を使っている。
多分入力ソース「日本語」が持っている日本語配列の設定情報が反映されて「日本語(かな漢字)」も日本語配列に置き換わる動きをしているような気がするので、その大元の設定の仕組みが分かれば根本的な解決ができそうな気がする。
ただ根本対処ならずとも実質恒久対処できたのでこの件はこれでクローズ。
SwiftUI で List の末尾を削除したらクラッシュする問題の対処
前置き
Objective-C から Swift に移行しようと腰を上げ始めたぐらいでそのまま2年が経過し、その間に SwiftUI まで登場した。せっかくなので SwiftUI のサンプルを作成してみたが、しょうもないところで躓いた。割と引っかかりやすいミスだと思う割には初歩的すぎるのかググってもあまり出てこないので、問題と対処についてメモを残す。
サンプルプログラムの概要
表示された行をタップすると表示文字を編集できる画面に遷移する。スライドで行削除できる。
クラッシュが起こる操作
このサンプルプログラムは、下記の操作を行うとクラッシュする。
1. 最終行をタップして画面遷移
2. 最初の画面に戻る
3. 最終行を削除する
クラッシュした際には下記のように Index out of range のメッセージが出る。
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
問題のプログラム
ContentView.swift
import SwiftUI struct ContentView: View { @State var strArr:[String] = ["aaa", "bbb","ccc"] var body: some View { NavigationView { VStack { List { ForEach(strArr.indices, id: \.self) { index in NavigationLink(destination: SecondView(strArr: $strArr, index: index)) { Text(strArr[index]) } } .onDelete(perform: deleteTest) } .listStyle(PlainListStyle()) } .padding() } } func deleteTest(at offsets: IndexSet) { strArr.remove(atOffsets: offsets) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
SecondView.swift
import SwiftUI struct SecondView: View { @Binding var strArr: [String] //ContentViewで保持している配列 @State private var editFlag = false //編集画面の切り替えフラグ let index : Int //strArrの要素を指すindex var body: some View { VStack { if(editFlag) { Button(action: { editFlag.toggle() }){ Text("完了") } HStack { Text("文字: ") TextField("入力", text: $strArr[index]) .textFieldStyle(RoundedBorderTextFieldStyle()) } } else { Button(action: { editFlag.toggle() }){ Text("編集") } HStack { Text("文字: ") Text(strArr[index]) Spacer() } } } .padding() } } struct SecondView_Previews: PreviewProvider { static var testData : [String] = ["aa", "bb", "cc"] static var previews: some View { SecondView(strArr: .constant(testData), index: 1) } }
クラッシュする原因
結論から述べるとクラッシュする原因は、遷移後の画面の文字列表示に、最初の画面で利用している@Stateな変数をそのまま利用しているからである。
具体的には、ContentView のプロパティである下記の
@State var strArr:[String] = ["aaa", "bbb","ccc"]
を、SecondView で @Binding で受け取り、それをText()などの表示でそのまま使っていることが原因。
@Binding var strArr: [String] ……… Text(strArr[index])
なぜ遷移後のViewである SecondView で、遷移前のViewのプロパティである strArr をそのまま表示に使うことでエラーが発生するかというと、@State の仕組みが関係してくる。
まず動作の流れを見ていくと、最終行(3行目)をタップして SecondView に画面遷移をした時、SecondView の プロパティの index には 2 の値が格納される。
なので Text(strArr[index]) では、strArr[2] に格納されている "ccc" が使われる。
その後、元の画面の ContentView に戻り最終行を削除すると、strArr[2] の値 "ccc" が削除され、strArrは[0]と[1]の2つの要素だけになる。
ここで @State の仕組みが関係してくる。
@State の属性を付与したプロパティは値に変更がかかると View が再描画される。
それは、@State属性の strArr を @Binding で使っている SecondView でも同じで、こちらも strArr の値が変更されると自動で再描画が発生する。
そのため strArr の[2]を削除した際、ContentView および SecondView で再描画が発生する。(なぜ今は表示していない SecondView まで再描画されるのかは調査できていないが、さっき表示していた SecondView の再描画も走っている)
そして、SecondView で再描画が走る際 index の値は 2 のままのため、Text(strArr[index]) の処理で、既に存在していない strArr[2] にアクセスしようとしてしまい、Index out of range でクラッシュする。
個人的には、ContentView 上での削除処理だったのでまさか SecondView まで再描画が走るとは思っておらず、この問題にしばらく躓いてしまった。もしかすると、1つ前の画面は少なくともまだアプリ的に生きている画面として保持されていて、それで更新対象になっているのかもしれない。
この問題の対処を色々試していくうち、3通りの解法を考えた。
3通りの方法どれでも対処できるが、対処方法その3が個人的に最もよいかと思う。
対処方法その1
修正作業量的に最もシンプルな対処パターン。
SecondView.swift の body に、 if文を1つ追加するだけ。
似たような事象のQ&Aにあった方法を参考にさせて頂いた。
var body: some View { VStack { if(index < strArr.count) { //ここを追加 if(editFlag) { Button(action: { editFlag.toggle() }){ Text("完了") } HStack { Text("文字: ") TextField("入力", text: $strArr[index]) .textFieldStyle(RoundedBorderTextFieldStyle()) } } else { Button(action: { editFlag.toggle() }){ Text("編集") } HStack { Text("文字: ") Text(strArr[index]) Spacer() } } } } .padding() }
SecondView で当該エラーが発生するのは、SecondView のプロパティの index の値が最終行のまま固定されていて、その index が既に存在しない配列の要素を使おうとするからである。
なので、index の値が現在の配列の要素数より小さい場合のみ描画をするようにすれば、当該エラーは発生しないというロジック。
やや雑さを感じるが、もう画面を使わなくなった後に起きる事象の対処なので細かいことは考えなくてよい性質を利用した合理的な対処。
対処方法その2
根本対処に臨んだ対処パターン。
SecondView での画面表示に使うプロパティは、全て SecondView が持つものを使うように変更し、根本的に当該エラーの発生をなくしている。
修正量は多いが、本来はこうすべきだったのだろうと思う。
struct SecondView: View { @Binding var strArr: [String] //ContentViewで保持している配列 @State private var editFlag = false //編集画面の切り替えフラグ @State private var inputStr:String //textFieldで使う変数。init()で初期化する let index : Int //strArrの要素を指すindex //初期化。inputStrを初期化したいがためだけに用意。 init(strArr: Binding<[String]>, index: Int) { self._strArr = strArr self.index = index self._inputStr = State(initialValue: strArr[index].wrappedValue) } var body: some View { VStack { if(editFlag) { Button(action: { editFlag.toggle() }){ Text("完了") } HStack { Text("文字: ") TextField("入力", text: $inputStr) .textFieldStyle(RoundedBorderTextFieldStyle()) .onDisappear { strArr[index] = inputStr } } } else { Button(action: { editFlag.toggle() }){ Text("編集") } HStack { Text("文字: ") Text(inputStr) Spacer() } } } } }
今回原因となっていた箇所は Text()で使っていた strArr なので、そこを SecondView が持つプロパティ inputStr に置き換える。
たまたま今回の原因になっていなかったTextField()の部分も、同様に inputStr に置き換える。
inputStr には、strArr[index] の値で初期化したかったため、init()を新たに作成して初期化している。
なお、今回追加したプロパティの inputStr は上記では private にしているが、そのせいで init処理を追記する必要が発生しており、それが面倒であれば private にはせず、ContentView から NavigationLink で飛んでくる際に引数で受け取ると楽ができる。
対処法その2では ContentView のコードを変更せずに対処する方法を検討したため、上記のようなやり方になっている。
対処方法その3
対処方法2の考えを引き継いだ上で、実用ベースで改良した対処パターン。多数変更を加えている。
そもそも String の配列単体だけを遷移先に渡すということ自体、実際のアプリのコードでは起きないと思う。
渡すのであれば Stringを包含した struct か class のプロパティが現実的だろう。
ということで、Stringを包含したstruct に置き換え、それベースでコードを見直した。
ContentView.swift
import SwiftUI struct TestStr: Identifiable { let id = UUID() var str: String } struct ContentView: View { @State var strArr:[TestStr] = [TestStr(str: "aaa"), TestStr(str: "bbb"), TestStr(str: "ccc"),] //前回までの [String] から [TestStr] に変更 var body: some View { NavigationView { VStack { List { ForEach(strArr) { tmp in NavigationLink(destination: SecondView(strArr: $strArr, tmpStr: tmp)) { Text(tmp.str) } } .onDelete(perform: deleteTest) } .listStyle(PlainListStyle()) } .padding() } } func deleteTest(at offsets: IndexSet) { strArr.remove(atOffsets: offsets) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
SecondView.swift
import SwiftUI struct SecondView: View { @Binding var strArr: [TestStr] //ContentViewで保持している配列 @State var tmpStr : TestStr //表示用に使うデータ @State private var editFlag = false //編集画面の切り替えフラグ var index : Int { //strArrの要素を指すindex strArr.firstIndex(where: { $0.id == tmpStr.id})! } var body: some View { VStack { if(editFlag) { Button(action: { editFlag.toggle() }){ Text("完了") } HStack { Text("文字: ") TextField("入力", text: $tmpStr.str) .textFieldStyle(RoundedBorderTextFieldStyle()) .onDisappear { strArr[index].str = tmpStr.str } } } else { Button(action: { editFlag.toggle() }){ Text("編集") } HStack { Text("文字: ") Text(tmpStr.str) Spacer() } } } .padding() } } struct SecondView_Previews: PreviewProvider { static var testData : [TestStr] = [TestStr(str: "aaa"), TestStr(str: "bbb"), TestStr(str: "ccc")] static var previews: some View { SecondView(strArr: .constant(testData), tmpStr: testData[1]) } }
まず、String のプロパティを持つ下記の構造体を定義。
ForEachで楽するために、Identifiableプロトコルに準拠する。
struct TestStr: Identifiable { let id = UUID() var str: String }
先ほどまで使っていたString配列の strArr は、TestStr配列のプロパティとして置き換える。
SecondView のプロパティも同様に変わり、また strArr が id を持つようにもなっているので、ContentView からの遷移で直接要素の値を受け取るのではなく直接対象のTestStr の値を受け取り(tmpStr)、tmpStr の id と strArrの各要素の id とを比較することで、index を求めるように変更している。
var index : Int { //strArrの要素を指すindex strArr.firstIndex(where: { $0.id == tmpStr.id})! }
SecondViewのプロパティが変更になり配列の要素番号を直接受け取らなくなったこと、および TestStr が Identifiable に準拠していることから、 ContentView の List の中の ForEach も index でのループから変更になっている。
ForEach(strArr) { tmp in NavigationLink(destination: SecondView(strArr: $strArr, tmpStr: tmp)) { Text(tmp.str) } }
まとめ
まだ SwiftUI は触り始めたばかりで、すでに表示していないViewの再描画が走る理由など分からない点しかないが、以前より GUI コードを書くのはかなり楽になっていた。
何はともあれ、他の階層のViewの操作で削除される値を持つプロパティを、Viewの表示にそのまま使ってはいけないという教訓を得た。
これを意識していれば本事象は回避できる。
あと今回は、@State なプロパティを別のViewに渡すということをしているが、同様のことをしたい場合、本来は @State ではなく @ObservedObject なり @EnvironmentObject を使うべきだろうと思う。
自分用メモ(VBA ファイル読み込み)
概要
後で自分がコピペする用のメモ。
VBAでエクセルファイルを読み込む関数。
環境:
Excel for Mac 16.16.24
VBA 7.1
Sub OpenTest() Dim filePath As String, fileName As String filePath = "/XXX/YYY/ZZZ/" fileName = "XXXXXXXXX.xlsx" Dim readBook As Workbook Dim mainBook As Workbook Set mainBook = ActiveWorkbook '今のアクティブウインドウを記録 'ファイルの存在確認 Dim exists As String exists = Dir(filePath & fileName) If exists = "" Then MsgBox "ファイルが存在しません : " & fileName & vbCrLf & filePath, vbExclamation Exit Sub End If '既に開いているかチェック Dim tmpWB As Workbook For Each tmpWB In Workbooks Dim compResult As Integer compResult = StrComp(tmpWB.Name, exists, vbTextCompare) If compResult = 0 Then MsgBox "既に開いています : " & exists, vbExclamation Exit Sub End If Next tmpWB 'ファイルを開く Application.ScreenUpdating = False '画面更新を止める Set readBook = Workbooks.Open(filePath & fileName, ReadOnly:=True) ActiveWindow.Visible = False '非表示にする mainBook.Activate ActiveWindow.Visible = True Application.ScreenUpdating = True 'ファイルを閉じる Application.DisplayAlerts = False readBook.Close Application.DisplayAlerts = True End Sub
VBAでIPv4アドレスを扱うコードの部品
概要
後で自分がコピペする用のメモ。
SWのACL絡みのチェック等をエクセル上で行うことが業務上増えたので、IPv4アドレスあたりに関するVBAのパーツをいくつか用意した。
人によっては何かに使えるかもしれない。
※元々は作成中のクラスのコードから抜き出したものなので、いきなり記載していないメンバ変数がコード中に現れているかもしれない。できる限りその影響はないように編集はしたものの、その状態で実行できるかは未確認。
コードの部品
・サブネットマスク32種類の情報を作る
'サブネットマスク構造体 Type subnetmask subnetmaskStr As String ' 文字列版サブネットマスク subnetmaskByte(4) As Byte End Type Dim subnetmaskList(32) As subnetmask ' サブネットマスク構造体の配列 。要素番号-1がそのままプレフィックスを示 Const base128 As Byte = 128 Dim mask As Byte, i As Long mask = 0 For i = 0 To 7 mask = mask + base128 ¥ (2 ^ i) subnetmaskList(i).subnetmaskByte(0) = mask subnetmaskList(i).subnetmaskByte(1) = 0 subnetmaskList(i).subnetmaskByte(2) = 0 subnetmaskList(i).subnetmaskByte(3) = 0 subnetmaskList(i).subnetmaskStr = subnetmaskList(i).subnetmaskByte(0) & "." & subnetmaskList(i).subnetmaskByte(1) & "." & subnetmaskList(i).subnetmaskByte(2) & "." & subnetmaskList(i).subnetmaskByte(3) subnetmaskList(i + 8).subnetmaskByte(0) = 255 subnetmaskList(i + 8).subnetmaskByte(1) = mask subnetmaskList(i + 8).subnetmaskByte(2) = 0 subnetmaskList(i + 8).subnetmaskByte(3) = 0 subnetmaskList(i + 8).subnetmaskStr = subnetmaskList(i).subnetmaskByte(0) & "." & subnetmaskList(i).subnetmaskByte(1) & "." & subnetmaskList(i).subnetmaskByte(2) & "." & subnetmaskList(i).subnetmaskByte(3) subnetmaskList(i + 16).subnetmaskByte(0) = 255 subnetmaskList(i + 16).subnetmaskByte(1) = 255 subnetmaskList(i + 16).subnetmaskByte(2) = mask subnetmaskList(i + 16).subnetmaskByte(3) = 0 subnetmaskList(i + 16).subnetmaskStr = subnetmaskList(i).subnetmaskByte(0) & "." & subnetmaskList(i).subnetmaskByte(1) & "." & subnetmaskList(i).subnetmaskByte(2) & "." & subnetmaskList(i).subnetmaskByte(3) subnetmaskList(i + 24).subnetmaskByte(0) = 255 subnetmaskList(i + 24).subnetmaskByte(1) = 255 subnetmaskList(i + 24).subnetmaskByte(2) = 255 subnetmaskList(i + 24).subnetmaskByte(3) = mask subnetmaskList(i + 24).subnetmaskStr = subnetmaskList(i).subnetmaskByte(0) & "." & subnetmaskList(i).subnetmaskByte(1) & "." & subnetmaskList(i).subnetmaskByte(2) & "." & subnetmaskList(i).subnetmaskByte(3) Next 'For i = 0 To 31 ' Debug.Print "eee " & subnetmaskList(i).subnetmaskByte(0) & "." & subnetmaskList(i).subnetmaskByte(1) & "." & subnetmaskList(i).subnetmaskByte(2) & "." & subnetmaskList(i).subnetmaskByte(3) & "." 'Next
・サブネットマスク(xxx.xxx.xxx.xxx)をプレフィックスに変換する。
戻り値は1〜32。
Function SubnetmaskToPrefix(subnet As String) As Long Dim aa As Long, bb As Long Dim tmp As Variant Const base128 As Byte = 128 Dim mask As Byte Dim countPrefix As Long ' 結果のプレフィックスを格納 Dim resAnd As Byte ' And を取ったときの結果を一時的に格納 Dim endFlag As Boolean ' ループを抜けるかどうかのフラグ tmp = Split(subnet, ".") num = UBound(tmp) If num <> 3 Then Exit Function End If countPrefix = 0 endFlag = False For aa = 0 To 3 ' オクテット毎ループ mask = 0 For bb = 0 To 7 '8bit 分ループ mask = mask + base128 ¥ (2 ^ bb) resAnd = mask And CByte(tmp(aa)) ' 今のビット位置でAndをとる。同じ位置のbitが立っていれば、次のIFの結果はイコールになる。 'Debug.Print "mask : " & mask & " , resAnd : " & resAnd If mask <> resAnd Then 'And の結果が違えばそこでループを抜ける endFlag = True Exit For End If countPrefix = countPrefix + 1 Next If endFlag = True Then Exit For End If Next 'Debug.Print "prefix: " & countPrefix SubnetmaskToPrefix = countPrefix End Function
・IPv4 の文字列を受け取り、引数(参照)の配列にByte型で格納する。
Sub AddressStrToByte(ipv4str As String, ByRef resultArr() As Byte) Dim tmp As Variant, num As Integer tmp = Split(ipv4str, ".") num = UBound(tmp) If num <> 3 Then Exit Sub End If resultArr(0) = CByte(tmp(0)) resultArr(1) = CByte(tmp(1)) resultArr(2) = CByte(tmp(2)) resultArr(3) = CByte(tmp(3)) End Sub
・あるNW内に、あるIPv4アドレスが含まれるかチェックする。
引数のdata()はIPv4アドレスの各オクテットの値を格納したByte型で要素数4の配列。
引数のprefixは、data()のアドレスのプレフィックス。1〜32。
ipv4strは、調べたいIPv4アドレスの文字列。
data()のNW内に、ipv4strのアドレスが含まれているかチェックする。含まれていればTrueを返す。
また、前述のサブネットマスク32種類の情報(subnetmaskList(32) As subnetmask)が既に存在して、この関数内からアクセスできる前提。
Function checkBelogToNetwork(ByRef data() As Byte, prefix As Long, ipv4str As String) As Boolean Dim nwaddress(4) As Byte nwaddress(0) = data(0) And subnetmaskList(prefix - 1).subnetmaskByte(0) nwaddress(1) = data(1) And subnetmaskList(prefix - 1).subnetmaskByte(1) nwaddress(2) = data(2) And subnetmaskList(prefix - 1).subnetmaskByte(2) nwaddress(3) = data(3) And subnetmaskList(prefix - 1).subnetmaskByte(3) Dim ckIPByte(4) As Byte Call AddressStrToByte(ipv4str, ckIPByte()) Dim nwad2(4) As Byte nwad2(0) = ckIPByte(0) And subnetmaskList(prefix - 1).subnetmaskByte(0) nwad2(1) = ckIPByte(1) And subnetmaskList(prefix - 1).subnetmaskByte(1) nwad2(2) = ckIPByte(2) And subnetmaskList(prefix - 1).subnetmaskByte(2) nwad2(3) = ckIPByte(3) And subnetmaskList(prefix - 1).subnetmaskByte(3) If nwaddress(0) = nwad2(0) And nwaddress(1) = nwad2(1) And nwaddress(2) = nwad2(2) And nwaddress(3) = nwad2(3) Then checkBelogToNetwork = True Else checkBelogToNetwork = False End If End Function