(2010.10.18〜)

Visual Basic 2010 メモ

画像ビューアを開発中に気付いたことなど……いやあ、想像以上にバグの多い処理系なんだねえ。少なくとも、「不可解で不便な仕様」に頭を抱えているユーザーが非常にたくさんいることを発見した。しかし、有名なバグにはそれなりの対処法方も発見されていて、ネットで検索すると、意外に簡単に解決するものが多いのには驚いた。ユーザーが多いことは強みだねえ。


●キーボード操作とフォーカス移動 (2010.10.18)

プログラムをキーボードで操作できるようにするには、ショートカットキーを使うのが一般的。メニューバーのコンポーネントを貼り付けて、必要な機能の項目を作り、ショートカットキー設定をすればよい。たとえば、メニューにファイルオープンの項目を登録して、その項目のShortcutKeyプロパティにCtrl+Oを登録するといったカンジ。

  アクセラレーションキー(Altシーケンス)は、項目名の該当文字の前に「&」を付ける。よく忘れるので注意。処理系によって付ける記号が異なるので混乱する。なお、何も指定しないと、頭文字が自動的にアクセラレーションキーになるようだ。

ただし、この方法では「Ctrl+文字」または一部の特殊キーしか登録できない。もうちょっとキー入力の自由度を高めたい。併用キーなしの文字キーやカーソルキー等にも機能を割り振りたい。こういうときには、フォームのキー入力イベント(KeyDown)を使う。このイベントハンドラでキー入力をフックして、必要な処理をしたあと、キー入力をコントロールに渡さないようにすればよい。ほとんどのキーでこの方法が使える。

  1. FormのLoadハンドラでKeyPreview=Trueを設定
  2. FormのKeyDownハンドラ内でe.KeyCodeを取得して必要な処理を書く
  3. そのあとにe.Handled=Trueとして、キー入力が抜けないようにする
ところが、カーソルキーとTABキーだけは例外で、この方法が通用しない。カーソルとTABは、コントロール間のフォーカスの移動に使用されているので、キー入力に優先して処理されてしまい、キー入力イベントが発生しない。しかも悪い事に、Windowsのビューアは表示送り/戻しにカーソルキーを割り当てている。自作ビューアもこの操作と互換性を持たせたい。個人的にはPageUp/PageDownの方が慣れているし、PageUp/PageDownならば何の問題もなく処理できるのだが、発表を前提とするなら標準との互換性は重要。

こういうときは、ボタンのPreviewKeyEventハンドラでIsInputKey=Trueとすれば、タブやカーソルも通常のキー入力として見なされる。しかし、タブまでキー入力と見なされるのはちと困る。確かに、カーソルはキー入力として処理したいのだが、タブはフォーカスの移動のままにしておきたい。カーソルキーのみ例外的な処理をしたいわけだ。

で、まあ、いろいろ考えたんだが(フォーカス移動ルーチンを自作するとか(^^;)、結局、KeyCode=37/39(左右のカーソルキー)のときだけ、IsInputKeyをTrueにすればよいことを発見した。具体的には、たとえば、Button1_PreviewKeyDownの中で、

    If (e.KeyCode=37) or (e.KeyCode=39) Then e.IsInputKey = True Else e.IsInputKey=False
とすればよい。

なお、PreviewKeyDownイベントハンドラの宣言部のHandlesのパラメータには注意すること。一つのボタンに対して上記の設定を行えば、Handlesで指定されている他のコントロールでもこの設定が有効になる。しかも、Handlesはイベントハンドラ作成時に自動的に設定されるので、ユーザーが明示的に指定する必要もない。ただし、このイベントハンドラを作成したあとに追加したコントロールに関しては、手作業での追加が必要になる。


●FileSystem.GetFilesでファイルの一覧を取得する

え〜っと、古い方法としては、FileSystem.Dir("*.*")などで最初のエントリを取り出し、次のエントリからはFileSystem.Dir()で取り出す(Turbo PascalのFindFirst/FindNextそっくりじゃなイカ…)。でも、これは面倒。FileSystem.GetFilesで一括して取得する方が便利。

  ファイル操作には、FileSystemObjectというスクリプト用のオブジェクトもあるが、これはデフォルトのVB2010では使えない。ランタイムを追加すれば使えるのかも知れないが、私は成功しなかった。それに、基本的にテキストファイル専用のようだ。

ちなみに、「My.Computer.FileSystem.GetFiles」という形式で使用する。なお、M$のHPのサンプルソースには誤りがあるようだ。第二パラメータは「SearchOption.SearchTopLevelOnly」または「〜.AllDirectories」でないとダメらしい。

なお、.Net Framework2以降だと、「System.IO.Directory.GetFiles」がほぼ同等の機能のメソッドだが、パラメータの順番が違うようだ。なんか、こっちの方がスマートな気がするんだが、どちらを使うべきか?

本来なら、DelphiのFileListBoxみたいのがあると便利なんだが、どうもないようだ。ただし、GetFilesの内容をそのまま配列に読み込んで、ListBoxに配列ごとAddRangeすれば、ほぼ同じことができる。少なくとも、一つずつチマチマ扱う必要はない。以下は、M$のHPにあった例の抜粋(一つずつ取り出す場合は次項参照)。

    Dim files As String() = System.IO.Directory.GetFiles("C:\test", "*", System.IO.SearchOption.AllDirectories)
    ListBox1.Items.AddRange(files)

●GetFilesで複数の拡張子のファイルのリストを取得する (2010.10.18)

ディレクトリ内にあるファイルの一覧を取得するには、FileSystem.GetFilesメソッドを使う。GetFilesでは拡張子を指定することもできるが、指定できる拡張子は一つだけ。「|」や「;」で複数の拡張子を区切って指定してもエラーになる。これはちょっと不便。たとえば、画像ファイル(*.jpg, *.png, *.gif etc.)のリストを取得しようと思ったら、拡張子の数だけGetFilesを実行しなければならない。この問題をスマートに解決する方法はないようだが、文字列配列とFor Eachを使えば、多少すっきりとしたコードになる。下記の例では、D:\ImgDirというディレクトリ内にある*.jpg、*.png、*.gif、*.jpegファイルをListBox1に登録している。
    Dim exps() As string = {"*.jpg","*.png","*.gif","*.jpeg"}

    ListBox1.Items.Clear 'ListBox1の中身を初期化

    For Each ex As string in exps '拡張子を順次指定するループ
        For Each fname As String In My.Computer.FileSystem.GetFiles _ '該当ファイルを一つずつ取り出すループ
        ("D:\ImgDir", FileIO.SearchOption.SearchTopLevelOnly, ex)
            ListBox1.Items.Add(fname) 'ListBox1にファイル名を一つずつ追加
        Next
    Next


●画像の回転とimage.FromFileのファイルロック (2010.10.13)

そもそもの目的は、回転画像の表示の高速化。imageの回転設定(RotateFlip)は、実はデータ自体の並び変えは行わず、フラグを立てるだけで、表示段階で回転処理をしているようなのだ。そのため、表示する度に回転処理が必要となる。大きなサイズ(1000万画素クラス)の画像を表示する際には物凄く大きな負荷になる。少し設定を変えて再確認をするだけの作業に、毎回数十秒掛かってしまう。

なので、元データと、元データを90度回転させたデータを、それぞれ別データとして保持して、必要に応じて切り替えることにした。問題は、回転済みの形式で保持する方法が見つからないこと。元データを内部で編集しても、同じ物にしかならないようだ。回転してコピーすればデータ並びも変わると思ったんだが…

で、逃げ道として考えたのが、回転後のデータを外部ファイル(テンポラリファイル)に書き出し、再度読み込む方法。確信があったわけではないが、結果的にこれで上手くいった。

ところが、プログラム終了時に、このテンポラリファイルが削除できない。それどころか、起動中にテンポラリファイルを再利用することすらできず、プレビューする画像の数と同じだけのテンポラリファイルが必要になる。何と、image.FromFile(ファイル名)で読み込んだファイルは、プログラムが終了されるまでロックされる仕様。imageをdisposeしてもロックが外れない。仕様と言うよりバグだろう。同じ現象に泣いているプログラマが物凄く沢山いるようだ。

で、これに対処する方法としてM$が提示しているのが、image.FromStreamを使ってストリームで読み込む方法。コードは少し面倒になるが、確かにこの方法だとロックは掛からない。ところが、これで読み込むと、回転画像の表示が元の状態(表示の度に回転処理を行う)に戻ってしまう!う〜む、面妖な…

しょうがないので、FromFileで読む事は変えず、何とかロックを外せないか、いろいろと試してみた。一番可能性がありそうだったのは、最後にダミーファイルを読み込む方法。一つのimageに画像を複数回読み込んだ場合、最後のファイルにのみロックが掛かるようなので、最後にダミーファイルを読み込んだらどうだろうか? 方向性としては有力だったが、そう簡単ではなかった。読み込むだけでいいのか?表示は必要か?どのタイミングで読み込むのか?どのルーチンに置くのか?などなど、検討すべき条件が多く、本来の機能のジャマになるカンジなのだ。少なくとも、デストラクタでFromFileする程度では駄目。書き出したファイルをコピーするなどの方法も試してみたが効果なし。

で、最後に考えたのが、例外処理。要するにこれはバグなのだから、バグらしい対処の仕方の方がよい。フォームのCloseハンドラ内でKILL命令を使ってテンポラリファイルを削除するのだが、このKILL命令をtryブロックに入れて、IOExceptionを捕まえて、何もせずに終了することにした。これでうまくいくんだよね……まあ、確かに、最後の一つが消せないことはあるみたいだけど、全部消えることもある。何とも…

ということで、えらいこと苦労したんだが、そもそもの問題としては、回転表示が速ければ、あるいはまともな回転変換ができれば済む話で、そのあたりを再度検討はしてみたいとも思う…


●エディタのキーバインド

一応、かなり自由に割り当てができるみたいだ。問題は、割り当て作業が非常に面倒な点にある。なんで、こんなインターフェースにしたんだろうね。おまけに、機能名の表記が変で、なかなか見つかりゃしない。Ctrl+HをBSに、Ctrl+MをEnterに割り当てたいだけなんだが、えらいこと苦労した。

キー機能機能名備考
Ctrl+H BackSpace 1語削除 変な表記だ…
Ctrl+M Enter 行に改行を挿入 これまた変だ

BSをCtrl+Hに割り付けると、置換とバッディングするが、BSを優先すれば問題ない。 問題はCtrl+Mの方で、2ストロークキーに使われていて、しかも、かなり沢山ある。 でもって、特に2ストロークキーの第一キー指定があるわけではなく、 使われているキー登録を一つずつ潰していくしか方法がないようだ、オイオイ…

ま、とりあえず、ダイヤモンド・カーソルくらいはできるようになった、と…。 それだけでも、けっこうありがたい。


●OpenFileDialog

初期ディレクトリの設定例OpenFileDialog.InitialDirectory="d:\mydir"
フィルターの設定例OpenFileDialog.Filter="Images|*jpg;*.png;*gif"

フィルターの指定子の書式は次の通り

 項目名|拡張子;拡張子;…|項目名|拡張子;拡張子;…
もちろん、拡張子は一つだけでもよい
 "テキスト|*.txt|HTML|*.html"


●全画面表示

基本的に、Formの二つのプロパティで設定できる。
    WindowState = FormWindowState.Maximized
    FormBorderStyle = FormBorderStyle.None
ただし、これはフォーム全体を全画面表示するものであり、 フォーム上のコンポーネントは全て表示される。 もし、PictureBoxのみ全画面表示にして、 他のコンポーネントを隠したいなら、 隠すコンポーネントのサイズやVisibleを変更する必要がある。 今回私が作成したプログラムの場合、 キモはTableLayoutPanelのサイズ変更で、これは、
    TableLayoutPanel.RowStyle(1)=0
といったカンジで余分な行のサイズを0にする必要があった。


●Exif情報の基本的な構成

Exif情報は、基本的に次のような構造をしている。
  [タグID][データのタイプ][データの長さ][データの内容]
[タグID]というのは、データの種類のことで、カメラの機種名とか、絞り値とか、露出補正などを示す、予め決められた数値が入っている。例えば「272」は「カメラの機種名」、「33434」は「シャッター速度」、「36867」は「撮影日時」。

[データのタイプ]というのは、端的に言えば数値か文字列か、という区別。これも予め決められた数値で表わされる。例えば、「Type2」は文字列、「Type3」は2バイト整数。問題は数値を表わす方法で、Exifでは基本的に整数形式しか扱えない。写真で扱う数値は整数とは限らないのだが、プログラム言語の浮動小数点型のような形式は採用されてないらしい(メーカーが独自拡張していれば別だが)。「Type5」や「Type10」は実数を扱う形式だが、これらは二つの整数を組み合わせて実数を表現している。

[データの長さ]は文字通りデータのバイト長。

[データの内容]は、数値の場合はデータの内容そのもの、文字列の場合は内容が格納されている場所のアドレスを示すポインタになることが多い。

●Exif情報の取得

昔はサードパーティのルーチンが必要だったみたいだが、 今はImage.PropertyItemsから取得できる(.NETの機能だろうね)。 ただし、いきなり特定のプロパティ(Exif情報)を抜き出すことは困難で、 はじめに全プロパティ項目を取得し、 そこから各プロパティを切り出していかなければならないようだ…。 ふ〜む、面倒な…。 ただし、プログラム自体はそんなに難しくない。
Dim img  as Image = Image.FromFile("p:\test.jpg")	画像の読み込み
Dim item as System.Drawing.Imaging.PropertyItem		各プロパティを入れる変数

For Each item in img.PropertyItems

    ListBox1.Items.Add(item.id)		プロパティの識別番号(タグ)
    ListBox1.Items.Add(item.type)	プロパティのタイプ(2は文字列)
    ListBox1.Items.Add(item.len)	プロパティの値の長さ

    If item.type=2 then		タイプ2のプロパティの値を文字列化して表示
        ListBox1.Items.Add(System.Text.Encoding.ASCII.GetString(item.value))
    End if
Next
前述のように、id(タグID)、type(データタイプ)、len(データ長)、val(データ値)の4つの項目が取得できる。type2以外は数値、type2は文字列だがそのままでは読めないので(単純にバイト配列として扱われる)、ASCII.GetStringで通常の文字列に変換している。また、文字列の場合、本来valの値は文字列の場所を示すポインタだが、このメソッドでは自動的に文字列の内容を返してくれる。

ただし、このPropertyItemsにはバグがあるようで、一部のデジカメの画像のExif情報が読み取れないことがある。少なくとも、東芝Allegretto 2300のメーカー名や機種名は読み落としている。バイナリエディタで画像データの中身を覗くと、メーカー名も機種名もきちんと入っているし、Windows 7のファイルのプロパティでも表示される。PropertyItemsのバグとしか考えられない。Exifの構造はそんなに複雑ではないので、自分で読み取りルーチンを作るのも悪くはないかも…

【PCうそつき講座目次】 【ホーム】