ユーザーインターフェイスの「Windowsらしさ」と「使い勝手」を考えよう
酒井法雄 SAKAI,Norio
norio@int21.co.jp
Visual Basicは積み木のようにコントロールを貼り付け,イージーにWindowsアプリケーションを作ることができるツールだ.しかし,それゆえにWindowsもつ特長を活かすことができない,ヘンなインターフェイスを作ってしまいがちでもある.
ここでは,「Windowsらしい」アプリケーションを作るための定石について,そして「Windowsらしくないが使いやすい」アプリケーションのアイデアについて,それぞれ設計やユーザーインターフェイス,そして実装方法などを考えてみる.
| 「Windowsらしさ」とは? |
|---|
「Windowsらしいアプリケーションだ」とか「Windowsでこういうインターフェイスはよくない」あるいは「こういった処理はWindowsには向かない」と言われることがある.そして言葉こそよく聞くものの,実際にWindowsでのアプリケーション開発をさんざんやってきたプロフェッショナルであっても,意外に正しく理解できていないことが多い.
本来は,「Windowsらしくない」アプリケーションをWindowsで作るのは邪道であり,そもそもWindowsを使う意味がないということもある.しかし,「Windowsらしくない」アプリケーションであっても,使い勝手さえよければそれでよしという考え方もある.
では,「Windowsらしい」とは,一体どういうことだろうか.
ここでは,主にインターフェイスについて「Windowsらしさ」とは何かについて述べ,そのメリットとデメリットを考えてみよう.
普段から何気なく使っているWindowsであるから,逆に「Windowsらしいインターフェイス」と言われても首をひねってしまう方も多いのではないだろうか.もし,あなたが仕事でWindows用のアプリケーションを作っているとしたら,これは非常にマズいことと言わざるを得ない.これは,クルマの免許も持ってないまま,オートマチックの280ps国産車で第三京浜を爆走するようなものである.確かに200km/h以上の速度を出せるかもしれないが,それはあなたが運転しているのではなく,クルマに乗せられているだけなのだ.
では,その免許とは何だろうか? 実はカンタンな話で,「Windowsユーザーインターフェイスデザインガイド」(アスキー刊)という本を読むことだ.この本はWindows 3.1まではSDKの一部となっていたものだ.つまり,Microsoftが決めたインターフェイスの規約なのである.
この本には,Windowsとはどのように使われることを想定してデザインされているか,そのためにプログラムはどうなっていなければならないかが事細かに書いてある.仕事でプログラミングしているわけではないという方には,この本はちょっと高いので必須とは申し上げにくいが,妙なAPIの使い方などを覚えるよりもずっと勉強になるとだけ申し上げておく.
「そんな本を読まなくても,普段からWindowsのアプリケーションを使っているのだから,それと同じにすればよいのだろう」と言われれば,確かにそうなのだが,それはこの本を読んだ人にこそ分かる「同じ」なのである.やはり,立ち読みぐらいはしておきたい.
実際には「なんだよ,Microsoftの製品でこの本の通りになっていないのもあるじゃん」ということもあるし,「いやー,とはいえこれじゃ不便じゃないかい?」ということもある.つまり,この本に書いてあることは,基本ラインであり,絶対的なものであると信奉するほどのものではないのだ.だが,この本で推奨されていること以外のユーザーインターフェイスを作ると,Visual Basicのバグがボロボロ出てきたり,あちこちで整合性が取れなくなってしまう.したがって,やはり一度は目を通しておくべきものなのだ.
というわけで,ここでユーザーインターフェイス自体について,いくら書いても,この本こそリファレンスなので,これ以上書くことはない.
と言ってしまうと実もフタもないので,ここから先は,この本で言うところの「Windowsらしいアプリケーション」について,それもごくオーソドックスな要素を満足するアプリケーションを作ってみよう.その過程で,実装するために必要となる「定石」について述べることにしたい.
| 今こそ「オーソドックスなアプリケーション」を見直すべきだ |
|---|
次に,オーソドックスなWindowsアプリケーションに要求されるものを,私なりにまとめてみた.
この他にもたくさんの項目があるが,まずはこんなところだろうか.
「なーんだ,みんな当たり前のことばかりじゃないか」と感じられた方も多いと思う.だが,この当たり前のアプリケーションをあなたは作っているだろうか.
Visual Basicでは,コントロールをペタペタと貼りつけていけるので,いきなりメインウィンドウにたくさんのコントロールが載っているアプリケーションを作ってしまいがちである.しかし,これでは初めてそのアプリケーションに触れた人にとっては,どこから何をはじめたらよいか,皆目見当がつかないということになりがちだ.
本来メインウィンドウはそのアプリケーションで主たる操作をするためのまな板に過ぎないのである.たとえば,メモ帳のシンプルさを思い起こして欲しい.これこそ,Windowsらしいアプリケーションの典型なのである.
ここで,例の本に書いてある「Windowsアプリケーションはこうあるべき」ということが,なぜ必要なのかを考えてみよう.
Windowsは,操作性をなるべく共通にして,初めてそのアプリケーションに触れた人でも,操作にとまどいを覚えないようにすることを,もっとも重要な開発目標としている.だが,そのためにはWindowsアプリケーションを作る開発者が,その点を十分に認知しておく必要があるのだ.
アプリケーションを開いたとき,ユーザーがどのように操作したらよいか迷わないように,見かけはシンプルにし,メニューの順番なども決まりきったありがちなパターンにする.あるべきところにあるメニューを操作していけば,思ったような操作が間違いなくできる.そうなっているべきなのだ.
ところが,実際にはアプリケーションの高機能化,さらにはWindows95以降のGUIからOOUI(Object Oriented User Interface)への方向性の転換があり,操作性は複雑化の一途をたどっている.Microsoft Officeを例に取るまでもなく,例の本を逸脱したインターフェイスをMicrosoft自体が作り出し,次にはそれを標準だと言い張っている.これはどうかしていると思われてもいたしかたないだろう.
私のようにWindowsの比較的初期段階から関わってきた者にとっては,これこそMicrosoftがOSとアプリケーションの両方を作っている独占の弊害にほかならないように思える.
そういうこともあり,ここではごくオーソドックスなインターフェイスを見直したいのである.もちろん,それだけではいけないのはじゅうじゅう承知であるが,まずはこのような基本,すなわち「初めてそのアプリケーションに触れる人にやさしいインターフェイス」を押さえておき,その上で「中上級者にとって現実的に使いやすいインターフェイス」を考えてみたい.
| 「Windowsらしい」ピクチャービュアーの仕様 |
|---|
ここでは,比較的カンタンな例として,メモ帳相当のものを作ることも考えたが,あまり面白くなさそうなので,ピクチャービュアーを作ってみた.ここでは,次のような仕様を考えた.
表:ピクチャービュアーのためのメニュー構成
| ファイル | 新規...ピクチャーをクリアする | 開く...ダイアログでファイルを選べる | 終了...アプリケーションの終了 |
| 編集 | コピー...クリップボードへのコピー | 貼り付け...クリップボードからの貼り付け | 壁紙にする...表示しているピクチャーを壁紙にする |
| オプション | 左ボタンドラッグでコピー...OLEDropされたときのデフォルト動作 | 左ボタンドラッグで移動...OLEDropされたときのデフォルト動作 | 左ボタンドラッグで表示のみ...OLEDropされたときのデフォルト動作 |
| ヘルプ | バージョン情報 |
プレビュー付きファイル選択ダイアログ,壁紙,OLE Drag&Dropの機能は,オーソドックスとは言い難いが,ちょっと面白い機能としてつけてみることにした.
次に,このアプリケーションの中で必要となる各機能についての定石を述べよう.
メニューデザインの定石
メニューは,メニューデザインウィンドウで作ることになっている.[Ctrl]-[E]で素早く起動することができる.
ここで気をつけるべきことは,キャプションの付け方だ.キーボードから素早く操作できるように,「開く(&O)...」などのように,アクセスキーの指定を&を使ってしておく.この例だと,ファイルメニューが開いていたときには,[Alt]-[O]で素早く選ぶことができる.また,このメニューの後にさらにダイアログなどでの操作が必要なときには,「...」を最後に付けておくこともお忘れなく.
よく使う機能については,アクセスキーとともに,ショートカットも指定しておくべきだ.これは,メニューが開いていないときでも,[Ctrl]-[O]やファンクションキーの組み合わせで直接その機能を呼び出すことができるものだ.つまり,少々慣れてきた中上級者向けの機能である.初めて使うアプリケーションでは,メニューをたどっていくしかないわけだが,慣れてきたらもっと素早くあの機能が使いたいということがあるだろう.そういうときのために,ショートカットを作っておくのだ.
また,名前はコントロールのオブジェクト名に相当するものだから,mnuなんとかというような分かりやすい名前をつけておこう.
ウィンドウスタイルの定石
BorderStyleプロパティは重要だ.メインウィンドウは,「可変」サイズであり,モーダルダイアログは「固定ダイアログ」である.モードレスのウィンドウは「固定(実線)」である.それぞれ,用途に合わせて設定する必要がある.
図1:ファイル選択ダイアログのデザイン
図2:プレビュー機能付きファイル選択ダイアログ
|
ファイル選択の定石
ファイルを選ぶダイアログは,コモンダイアログを使うという手がある.これにも定石はあるが,キャンセル時にエラーで知らせるというような不思議な仕様や,メソッドで効率的にダイアログを開けないといった仕様は,メソッドの使えなかったVBX時代の過去の遺産を引きずっていて使い勝手がよいものとは言えない.だが,不満を言わなければちゃんと使えるものではある.
しかし,今回のようにプレビュー機能付きのファイル選択ダイアログを作ろうというならば,自前で作ってしまうしかない(図1・2).これにはDrive,Dir,Fileの各コントロールを使う.
今どきTreeViewじゃないといったあたりはイマイチだが,余計なActiveXコントロールを使わなくて済むという点ではよい.昨今の状況を見ていると,ActiveXコントロールはバージョンが変わったときに仕様に不整合が出てくる可能性があるし,再配布時に面倒なことがおきがちなので,あまり使いたくない気持ちになる.本来は,再利用可能な部品として効果的に使いたいものなのではあるが…
これらのコントロールは内蔵コントロールなので,そのへんは安心だ.一般的には,
Drive1.Drive -> Dir1.Path -> File1.Fileのように連携を取っていけば,基本的にはうまく動く.ただし,いくつか注意すべき点がある.
Private Sub Drive1_Change() On Error Resume Next Dir1.path = Drive1.Drive If Err Then Beep Drive1.Drive = Left(Dir1.path, 2) End If End SubDirコントロールのPathプロパティはルートディレクトリにあるときには「C:\」だが,そうでないときには「C:\WINDOWS」のように,右端に「\」が付く付かないがあるので,統一を取っておいた方がよい.
Private Sub Dir1_Change() File1.path = Dir1.path End Sub Private Sub File1_Click() On Error GoTo trapLoadFile If Right(File1.path, 1) = "\" Then picFileName = File1.path & File1.filename Else picFileName = File1.path & "\" & File1.filename End If Me.Caption = "Selecting..." & picFileName picTmp.Picture = LoadPicture(picFileName) resLoadFile: Exit Sub trapLoadFile: imgPreview.Picture = LoadPicture() MsgBox Err.Description, vbCritical, "Error" Resume resLoadFile End Sub
ダイアログとのやり取りの定石
Public defPath As String Public fileName As StringOKかキャンセルかの区別をするために,ここではFileNameを空文字列にしてから,ダイアログをモーダルでオープンした.FileNameに何も入っていなければ,キャンセルだったということになる.
Private Sub mnuFileOpen_Click() FileName = "" frmFileSel.Show vbModal If FileName <> "" Then Caption = App.Title & " - " & FileName Screen.MousePointer = vbHourglass imgPicture.Picture = LoadPicture(FileName) ReCalcDisplay Screen.MousePointer = vbNormal End If End Subダイアログ側では,次のようにして初期値を設定することができる.
Private Sub Form_Load() Drive1.Drive = Left(frmDisp.defPath, 2) Dir1.path = frmDisp.defPath End Subまた,OKが押されたときには,次のようにメインウィンドウのプロパティを変更してやればよい.キャンセル時には,ただUnload Meすればよい.
Private Sub cmdOK_Click() frmDisp.filename = picFileName If Right(Dir1.path, 1) <> "\" Then frmDisp.defPath = Dir1.path & "\" Else frmDisp.defPath = Dir1.path End If Unload Me End Sub一般的には,ダイアログで設定された内容によって何かスグにしなくてはならないときには,モーダル表示した次からの行で作業をすればよい.しかし,次のようなやり方もある.
Public Property Let FileName(fname As String) tmpFileName = fname SetRecentFilesMenu fname End Property Public Property Get FileName() As String FileName = tmpFileName End Propertyこれは,単なる変数としてのプロパティではなく,プロパティプロシージャを作ったものだ.ここで,FileNameプロパティに新しいものが代入されると,「最近使ったファイル」の一覧に追加するといったことができる.この方法だと,たとえばDrag&Dropされたときや,「最近使ったファイル」からファイルを選ばれたときにも,とにかくFileNameプロパティに代入されただけで,「最近使ったファイル」のリストが更新される.こういう使い方は便利だ.
アスペクトを保ちつつリサイズする定石
ファイル選択ダイアログは固定サイズであるから,プレビューも一定サイズ内に表示しなくてはならない.しかし,イメージコントロールのStretchプロパティをTrueにしただけでは,アスペクト比を保つことができない.
そこで,一度AutoSizeをTrueに設定したピクチャーボックスに読み込み,アスペクト比を得る.その後,イメージコントロールにピクチャーを移し,そのアスペクト比でリサイズしてやればよい.
このとき,ピクチャーコントロールのResizeイベント発生時に,イメージコントロールのデザイン時のサイズを基準にして,縦横の長い方のサイズをそのままにして,短い方の長さを合わせてやれば,必ず範囲内に収めることができる.
Private orgWidth As Integer Private orgHeight As Integerイメージコントロールには,リサイズ後に絵を転送している.
Private Sub picTmp_Resize() If picTmp.Width > picTmp.Height Then imgPreview.Width = orgWidth imgPreview.Height = orgWidth * _ (picTmp.Height / picTmp.Width) Else imgPreview.Height = orgHeight imgPreview.Width = orgHeight * _ (picTmp.Width / picTmp.Height) End If imgPreview.Picture = picTmp.Picture End Sub以前に表示していたものと同じサイズの絵のときには,このままでは新しい絵が表示されない.そこで,ピクチャーコントロールのChangeイベントで強制的にResizeイベントを呼んでやった.
Private Sub picTmp_Change() picTmp_Resize End Sub
スクロールするピクチャーの定石
図3:メインウィンドウのデザイン
|
コンテナとその上のコントロールは,親子関係になる.つまり,コンテナを動かすと,その上にあるコントロールも同時に動くようになる.これは実にスムーズに動かすことが可能だ.
ここで,発想を変えて,子どもの方が大きかったらどうなるかを考えてみよう.子どもはあくまでも親であるコンテナの外に出られない.したがって,子どもの一部だけが表示されるようになる.あとは,子どものLeftとTopプロパティを,スクロールバーのValueプロパティに合わせて変わるようにしておけば,スクロールはOKである.
たとえば,picCont上に配置されたimgPictureであれば,次のようなコードで移動量,すなわちスクロールバーのMaxプロパティを決めることができる.
hsbPicture.Max = _ Abs(imgPicture.Width - picCont.Width) vsbPicture.Max = _ imgPicture.Height - picCont.Height
vsbPicture.LargeChange = _ vsbPicture.Max / 5 + 1 vsbPicture.SmallChange = _ vsbPicture.Max / 20 + 1スクロールバーのChangeイベントでは,imgPictureのLeftやTopプロパティを(対応するスクロールバーのValueプロパティ)×(-1)してやれば,適切な位置に移動させることができる.
Private Sub hsbPicture_Change() imgPicture.Left = -hsbPicture.Value End Sub Private Sub vsbPicture_Change() imgPicture.Top = -vsbPicture.Value End Sub
ウィンドウのリサイズに対応するスクロールするピクチャーの定石
Private Sub Form_Resize() If WindowState <> vbMinimized Then ReCalcDisplay End If End Subしかし,スクロールバーなどを含む,複数のコントロールとなると複雑になってしまう.というのも,スクロールバーが表示されるかどうかは内部のドキュメントの表示エリアによって変わるからだ.それも,水平と垂直のスクロールバーがあるかどうかで相互に影響することになる.
リスト1:スクロールバーの状態によって処理を設定する Private Sub ReCalcDisplay() Dim i As Integer Do If ScaleWidth < imgPicture.Width - vsbPicture. _ Visible * vsbPicture.Width Then hsbPicture.Visible = True hsbPicture.Move 0, ScaleHeight - hsbPicture.Height, _ ScaleWidth + vsbPicture.Visible * vsbPicture.Width picCont.Height = Abs(ScaleHeight - hsbPicture.Height) hsbPicture.Max = Abs(imgPicture.Width - picCont.Width) hsbPicture.LargeChange = hsbPicture.Max / 5 + 1 hsbPicture.SmallChange = hsbPicture.Max / 20 + 1 Else hsbPicture.Visible = False picCont.Height = ScaleHeight imgPicture.Left = 0 End If If ScaleHeight < imgPicture.Height - hsbPicture. _ Visible * hsbPicture.Height Then vsbPicture.Visible = True vsbPicture.Move ScaleWidth - vsbPicture.Width, _ 0, vsbPicture.Width, Abs(ScaleHeight + _ hsbPicture.Visible * hsbPicture.Height) picCont.Width = ScaleWidth - vsbPicture.Width vsbPicture.Max = imgPicture.Height - picCont.Height vsbPicture.LargeChange = vsbPicture.Max / 5 + 1 vsbPicture.SmallChange = vsbPicture.Max / 20 + 1 Else vsbPicture.Visible = False picCont.Width = ScaleWidth imgPicture.Top = 0 End If i = i + 1 Loop While i < 2 End Sub |
クリップボードとのやり取りの定石
クリップボードとやり取りするときには,[編集]メニューに,[コピー][切り取り][貼り付け]といったメニューを付ける.それぞれショートカットは,[Ctrl]-[C],[Ctrl]-[X],[Ctrl]-[V]である.
このキーアサインはWindows 3.1からで,実はMacintoshのマネである.キーボードにこれらの文字が並んでいることから分かりやすいということで,Windowsでも採用されたようだ.しかし,私のようにWordStar配列のエディタに慣れていると,これらのキーアサインがバッティングしてしまうため,Windowsでの使い勝手が非常に落ちてしまう.現実的には,WordStar配列のエディタと,他のアプリケーションを併用することは困難である.こういったOSレベルで共通なキーアサインを,OSレベルでまとめてカスタマイズできないのは,今のWindowsに対する大きな不満点である.
それはともかく,クリップボードとのやり取りは,Clipboardオブジェクトのメソッドを使って行う.
Private Sub mnuEditCopy_Click() Clipboard.Clear Clipboard.SetData imgPicture.Picture End Sub Private Sub mnuEditPaste_Click() Screen.MousePointer = vbHourglass imgPicture.Picture = Clipboard.GetData() ReCalcDisplay Screen.MousePointer = vbNormal End Sub
OLD Drag & Dropの定石
図4:右ボタンでOLD Dropしたときのポップアップウィンドウ
そこで,動作のデフォルトをオプションメニューでチェックして設定できるように,デザインした.これらはコントロール配列になっている.
Begin VB.Menu mnuOption Caption = "オプション(&O)" Begin VB.Menu mnuOptionDrop Caption = "左ボタンドラッグでコピー(&C)" Checked = -1 'True Index = 0 End (以下略)メニューが選ばれたときには,一度すべてのチェックを外し,選ばれたIndexのものだけにチェックを付けてやる.ちょっとしたことではあるが,If文を使わないループになるので高速なのがポイントだ.
Private Sub mnuOptionDrop_Click(Index As Integer) Dim i As Integer For i = 0 To 2 mnuOptionDrop(i).Checked = False Next i mnuOptionDrop(Index).Checked = True End Sub後で左ボタンDrop時にデフォルト動作をするように判断する.これはモジュールレベルの変数として保持することもできるが,次のようにループで回してチェックしてもよい.
' DragDrop時のデフォルトを調べる For j = 0 To 2 If mnuOptionDrop(j).Checked Then Exit For End If Next jこの部分については,”メニューでチェック項目を作るときの定石”とも言えるだろう.
Begin VB.Menu mnuPopup Caption = "Dummy" Visible = 0 'False Begin VB.Menu mnuPopupAction Caption = "ここにコピー(&C)" Index = 0 End (以下略)このメニューを表示するには,次のようにPopupMenuメソッドを使う.引数としてメニューオブジェクト名,選択するボタン,表示位置,さらに太字で表示されるデフォルトのメニューの指定ができる.
PopupMenu mnuPopup, vbPopupMenuRightAlign Or _ vbPopupMenuRightButton, , , mnuPopupAction(j)メニューが選ばれたときには,選ばれたIndexをモジュールレベル変数selActionに保持している.
Private selAction As Integer Private Sub mnuPopupAction_Click(Index As Integer) selAction = Index End SubここまでがOLEDrag&Dropを使うための下準備である.ここからが本番だ.
Private Sub imgPicture_MouseDown(Button As Integer, _ Shift As Integer, X As Single, Y As Single) imgPicture.OLEDrag End SubOLEDragが開始されると,該当コントロールのOLEStartDragイベントが発生する.
Private Sub imgPicture_OLEStartDrag( _ Data As DataObject, AllowedEffects As Long) ' フォーマットのみ指定しておく Data.SetData , vbCFFiles ' エフェクトを指定しておく AllowedEffects = vbDropEffectMove Or _ vbDropEffectCopy End Sub相手側にDropされると,OLESetDataイベントが発生し,実データが要求される.ここで,要求されたものがvbCFFilesであることを確かめてから,DataオブジェクトのFilesオブジェクトのAddメソッドで,ファイル名を渡してやる.
Private Sub imgPicture_OLESetData( _ Data As DataObject, DataFormat As Integer) ' vbCFFilesのリクエストがきたらデータを設定する If DataFormat = vbCFFiles Then Data.Files.Add FileName '& vbCrLf End If End Sub相手側でDropが終了したら,OLECompleteDragイベントが発生する.ここで,相手側で選ばれたエフェクトが移動であったなら,元々あったファイルを消すのは相手側の仕事である.しかし,表示されている内容までは相手側から消すことはできない.そこで,このイベントでピクチャーをクリアしてやる.
Private Sub imgPicture_OLECompleteDrag(Effect As Long) If Effect And vbDropEffectMove Then imgPicture.Picture = LoadPicture() End If Screen.MousePointer = vbNormal End Sub次に,相手側からのDrag&Dropを考えよう.
Private lastButton As Integer Private Sub imgPicture_OLEDragOver _ (Data As DataObject, Effect As Long, _ Button As Integer, Shift As Integer, _ X As Single, Y As Single, _ State As Integer) ' OLEDragDropでButtonが ' 0になるバグの回避 lastButton = Button End SubDragDropイベントでは,vbCFFilesであることを確認した上で,DataオブジェクトのFilesコレクションの内容をひとつずつ取得していく.ここで,先ほど出てきた相手側のOLESetDataイベントが発生すると思えばよい.ここで,各ファイルをパス名とファイル自体の名前に分割しておく.
リスト2:DragDropイベントの処理 Private Sub imgPicture_OLEDragDrop( Data As DataObject, _ Effect As Long, Button As Integer, _ Shift As Integer, X As Single, Y As Single) Dim src As String Dim ffile As String Dim i As Integer Dim j As Integer Dim tmppath As String Dim tmpfile As String Dim tmpfullpath As String Screen.MousePointer = vbHourglass ' 空でフォーマットだけ指定してあったので次の行は通る If Data.GetFormat(vbCFFiles) = True Then On Error Resume Next For i = 1 To Data.Files.Count SplitPath Data.Files(i), src, ffile ' 自分のディレクトリのときはパス If src = defPath Then Effect = vbDropEffectNone Exit For End If ' DragDrop時のデフォルトを調べる For j = 0 To 2 If mnuOptionDrop(j).Checked Then Exit For End If Next j Screen.MousePointer = vbNormal ' OLEDragDropでButtonが0になるバグの回避 Select Case lastButton Case vbRightButton PopupMenu mnuPopup, vbPopupMenuRightAlign _ Or vbPopupMenuRightButton, , , mnuPopupAction(j) Case vbLeftButton selAction = j Case Else Effect = vbDropEffectNone Exit For End Select Select Case selAction Case 0 'Copy Effect = vbDropEffectCopy tmpfullpath = defPath & ffile If CopyFiles(src & ffile, tmpfullpath, False) Then SplitPath tmpfullpath, tmppath, tmpfile FileName = defPath & tmpfile Caption = App.Title & " - " & FileName imgPicture.Picture = LoadPicture(FileName) End If Case 1 ' Move tmpfullpath = defPath & ffile If CopyFiles(src & ffile, tmpfullpath, True) Then SplitPath tmpfullpath, tmppath, tmpfile FileName = defPath & tmpfile Caption = App.Title & " - " & FileName imgPicture.Picture = LoadPicture(FileName) Effect = vbDropEffectMove Else Effect = vbDropEffectNone End If Case 2 ' Display only Effect = vbDropEffectNone FileName = src & ffile Caption = App.Title & " - " & FileName imgPicture.Picture = LoadPicture(FileName) Case Else ' Cancel Effect = vbDropEffectNone End Select Next i End If End Sub Public Sub SplitPath(src As String, path As String, _ FileName As String) Dim i As Integer For i = Len(src) To 1 Step -1 If Mid(src, i, 1) = "\" Then path = Left(src, i) FileName = Mid(src, i + 1) Exit For End If Next i End Sub |
ファイルのコピー,移動,リネームの定石
ファイルをコピーするには,Visual BasicのFileCopy関数を使う.ただし,この関数は上書き確認をしてくれないし,R/Oファイルなどのときのエラーのハンドリングもしてくれない.
そこで,こういった処理をまとめた関数を作ってみた.
この関数では,srcからdstにコピーし,killsrcがTrueのときにはsrcを削除する.また,コピーが成功したときにはTrueを返す.
内容としては,コピー先のファイルの存在をOpen関数でチェックし,存在したときには,メッセージボックスで上書きするかを確認する.Noのときには新しい名前を入れるように促してリネームする.しかし,ここで入れられた名前もすでに存在している可能性もある.そこで,この部分はうまくゆくまでのループになっている.
ループを抜けたところで,FileCopy関数を呼んでコピーをする.ここでなんらかの理由でエラーが起きたときにはエラーメッセージを出して終了する.
killsrcのときには,さらに元ファイルを削除して終わる.もちろんここでもエラートラップがある.
Public Function CopyFiles(src As String, _
dst As String, killsrc As Boolean) As Boolean
Dim fd As Integer
Dim r As Integer
Dim destpath As String
Dim destfile As String
On Error Resume Next
Do
fd = FreeFile
Open dst For Input As fd
If Err.Number = 0 Then ' File Exist
Close fd
r = MsgBox("上書きしますか? :" & dst & _
" <No> でリネームします.", vbCritical _
Or vbYesNoCancel, "ファイルが存在します")
Select Case r
Case vbYes
Exit Do
Case vbNo
SplitPath dst, destpath, destfile
dst = destpath & InputBox( _
"新しい名前を入れてください. '" & _
destfile & "'", "リネーム", destfile)
Case vbCancel
CopyFiles = False
Exit Function
End Select
Else
Exit Do
End If
Loop
Err.Clear
On Error GoTo 0
CopyFiles = True
On Error Resume Next
FileCopy src, dst
If Err Then
MsgBox "ファイルのコピーに失敗しました " & src & _
" から " & dst, vbCritical, "Error"
CopyFiles = False
' 失敗したときには削除しない
Exit Function
End If
If killsrc Then
Kill src
If Err Then
MsgBox "ファイルの削除に失敗しました." & _
"元ファイル: " & src, vbCritical, "Error"
' 一応成功
CopyFiles = True
End If
End If
End Function
図5:実行画面.最近使ったファイルが表示されている
|
最近使ったファイルを表示する定石
ファイルメニューに最近使ったファイルが出てくるのは便利なものだ.そこで,簡易的にこれを実現してみた(図5).
ここでは,表示するファイルのヒストリー数だけのメニューアイテムをあらかじめメニューエディタで作成しておいた.これはコントロール配列とし,VisibleはFalseに設定しておいた.なお,セパレータも同様にVisible = Falseである.
Begin VB.Menu mnuFileSep1 Caption = "-" Visible = 0 'False End (以下略)SetRecentFileMenu関数は,引数として受けた文字列をファイルのフルパス名としてメニューに追加する.このとき,それ以前のものはひとつずつシフトしなければならない.また,本当ならば同じファイル名があれば最新のものだけにすべきだが,このへんをやっていくと結構複雑になってしまったので,ここでは単純にシフトだけしている.このため,同じものが複数並ぶことがある.
Private Sub SetRecentFilesMenu(fname As String) Dim i As Integer If fname <> "" Then mnuFileSep1.Visible = True For i = 3 To 0 Step -1 If mnuFileRecent(i).Visible = True Then mnuFileRecent(i + 1).Caption = mnuFileRecent(i).Caption mnuFileRecent(i + 1).Visible = True End If Next i mnuFileRecent(0).Caption = fname mnuFileRecent(0).Visible = True End If End Subこの「最近使ったファイル」が選ばれたときにも,そのピクチャーを表示しなければならないので,ダイアログのオープン後のコードと同じコードを記述する.
Private Sub mnuFileRecent_Click(Index As Integer) FileName = mnuFileRecent(Index).Caption Caption = App.Title & " - " & FileName Screen.MousePointer = vbHourglass imgPicture.Picture = LoadPicture(FileName) ReCalcDisplay Screen.MousePointer = vbNormal End Sub
構造化の定石
Public Property Let FileName(fname As String) tmpFileName = fname If tmpFileName <> "" And Me.Visible Then Caption = App.Title & " - " & tmpFileName Screen.MousePointer = vbHourglass imgPicture.Picture = LoadPicture(tmpFileName ) ReCalcDisplay Screen.MousePointer = vbNormal End If SetRecentFilesMenu fname End Property Private Sub mnuFileOpen_Click() FileName = "" frmFileSel.Show vbModal End Sub Private Sub mnuFileRecent_Click(Index As Integer) FileName = mnuFileRecent(Index).Caption End Sub
アプリケーション終了時の定石
Private Sub mnuFileExit_Click() Unload Me End Sub Private Sub Form_Unload(Cancel As Integer) SaveSettings End Sub
アプリケーション名をまとめる定石
図6:プロジェクトのプロパティで変えられるアプリケーションの「タイトル」
App.Ttileを使ってメインウィンドウのキャプションを設定したり,「バージョン情報」を表示したりするには,次のようなコードを書けばよい.
Caption = App.Title & " - " & FileName Private Sub mnuHelpAbout_Click() MsgBox App.Title & " Version 1.0" & vbCrLf & "Copyright(C) 1998 Norio Sakai", vbInformation, App.Title End Sub
図7:レジストリに書かれた内容を見る
|
アプリケーション固有情報保存の定石〜レジストリ
アプリケーションが再起動されたとき,前回のオプション設定やウィンドウの位置や大きさなどが残っていた方がよい.以前のWindowsであれば,プライベートのINIファイルをWindows APIを使って読み書きするといった手法が取られた.
最近のVisual Basicでは,レジストリにこういった情報を読み書きする機能が最初から用意されている.ただし,レジストリのどこでも読み書きができるわけではない.実際に読み書きできるのは,「HKEY_CURRENT_USER\Software\VB and VBA Program Settings\」の下である(図7).それ以外にアクセスしたいときには,Windows APIを使う必要がある.
レジストリへの書き込みには,SaveSettingステートメント,読み込みにはGetSetting関数,GetAllSettings関数,削除にはDeleteSettingステートメントを使えばよい.基本的な使い方は,次のコードを見ればカンタンに理解することができるだろう.アプリケーション名,セクション名,キー名の組み合せに対しての内容といった形で扱うことができる.
しかし,レジストリの同じセクションにある一覧をまとめて扱いたいということがある.今回の例で言えば,最近使ったファイルなどの一覧を取得して,For〜Next文で回したいといったときだ.
書き込むときには,SaveSettingステートメントをループ中に書けばよいが,読み込むときにはいくつ要素があるか分からないので,決め打ちはマズい.
このようなときには,セクションまでを指定して,GetAllSettings関数を使う.この戻り値はリスト(配列)である.したがって,一度Variant型変数に代入し,要素をひとつずつ取り出すことができる.
リスト3:レジストリ情報の保存・取得 Private Sub SaveSettings() Dim i As Integer SaveSetting App.Title, "INIT", "DefaultPath", defPath SaveSetting App.Title, "INIT", "Left", Me.Left SaveSetting App.Title, "INIT", "Top", Me.Top SaveSetting App.Title, "INIT", "Width", Me.Width SaveSetting App.Title, "INIT", "Height", Me.Height For i = 0 To 4 If mnuFileRecent(i).Visible = True Then SaveSetting App.Title, "RecentFiles", _ CStr(i), mnuFileRecent(i).Caption Else SaveSetting App.Title, "RecentFiles", CStr(i), "" End If Next i For i = 0 To 2 If mnuOptionDrop(i).Checked = True Then SaveSetting App.Title, "OPTION", "DropAction", i Exit For End If Next i End Sub Private Sub GetSettings() Dim v As Variant Dim i As Integer defPath = GetSetting(App.Title, "INIT", "DefaultPath", App.path) If Right(defPath, 1) <> "\" Then defPath = defPath & "\" End If Me.Left = GetSetting(App.Title, "INIT", "Left", Me.Left) Me.Top = GetSetting(App.Title, "INIT", "Top", Me.Top) Me.Width = GetSetting(App.Title, "INIT", "Width", Me.Width) Me.Height = GetSetting(App.Title, "INIT", "Height", Me.Height) If Me.Left > Screen.Width Or Me.Top > Screen.Height Then Me.Move 0, 0 End If v = GetAllSettings(App.Title, "RecentFiles") If Not IsEmpty(v) Then For i = UBound(v, 1) To 0 Step -1 If v(i, 1) <> "" Then FileName = v(i, 1) End If Next i End If mnuOptionDrop_Click GetSetting(App.Title, "OPTION", "DropAction", 0) End Sub |
アイコンの定石
最後に,アプリケーションにはきちんとしたアイコンをつけよう.もちろん,フォームのICONプロパティに指定しておけばよい.アプリケーション自体のアイコンとするには,プロジェクトのプロパティを出し「実行可能ファイルの作成」タブの「アイコン」でどのフォームのアイコンを選ぶかを指定すればよい.
| 「Windowsらしくない」アプリケーションだっていいじゃない |
|---|
さて,「Windowsらしい」オーソドックスなアプリケーションを作るための定石を紹介してきた.実際には,「Windowsらしくない」ものにも応用できることも少なくはないが,ともかく「アプリケーションとしてまとまりのあるものを作るにはどうしたらよいか」ということは,いくばくかご紹介できたと思う.
しかし,このような優等生的なアプリケーションがいつも使い勝手がよいとは限らない.たとえば,今回作ったピクチャービュアーは,「選んで見る」ことが主たる目的である.ならば,いちいちメニューからダイアログを選んでフルサイズの絵を表示するより,このアプリケーションのダイアログ部分であるプレビューだけでもよいのではないだろうか(図8).
実際にこの部分だけを抜き出したプログラムも作ってみた.ファイルリストボックスからはOLEDragもできるようになっている.見ることが主体であれば,むしろこの方が使い勝手がよい.
図8:別ウィンドウに本来のピクチャーサイズで表示し,リサイズもできる
|
さらに,本来の大きさでも見たいというのであれば,「Windowsらしい」アプリケーションのメインウィンドウだった部分を,ファイル選択部分から呼び出せばよいではないか.しかし,このときにも「見ることが主眼」なのだから,スクロールだのリサイズに対応だの面倒なことを言うのではなく,いきなり元のサイズが見えるウィンドウが出た方がよいだろう.
ついでだから,画面サイズより大きくなったら,自動的に最大表示できる大きさにリサイズしたり,手動でもリサイズができ,アスペクト比も自在に変えられる方が面白い.
というわけで作ったのが,"PictureGear"(図9)である.コードについては次号のCD-ROMに収録予定のサンプルをご覧いただきたい.
ここでは,壁紙にする機能やら,クリップボードに転送する機能やらは,特定のキーを押したときに実行されるようになっている.メニューもガイドもないのは不親切きわまりないが,「裏技です」と言ってしまえば済むのがこの手のプログラムでもある.
実際に使ってみると,このような「Windowsらしくない」方がよっぽと使いやすいのは否定できない.つまり,主眼が何であるかによって,インターフェイスなど自由に変えるべきなのである.
これをさらに推し進めたのが,「素早くファイルを見て,いらないものは削除して,必要なものは系統別に適切なディレクトリにコピーし,リネームや削除もできる」という "PictureList" (図10)だ.
このプログラムでは,複数のコピー先を自由に指定できるように,コマンドボタンをコントロール配列で増減できるようになっている.そして,アクセスキーを使って素早くファイルの振り分けができる.
この部分でも本当はいろいろな定石があるのだが,ページも尽きてきたことだし,そこはコードをご覧いただきたい.
このプログラムは,実際に使うと非常に便利である.インターネットからダウンロードした大量のピクチャー(笑)があるという方には,オススメである.
図9:コピー/リネーム/削除などを素早く実行できるPicture Gear.複数のコピー先ボタンを動的に作ることができる
図10:PictureListの実行例.元絵のサイズを個々に表示可能.OLE Drag&Drop,表示サイズの変更,
リネーム,削除にも対応.複数インスタンス間でビジュアルにファイルのやり取りができる
しかし,「Windowsらしい」方も負けてはいない.次に紹介するのは,(長いこと)デバッグ中のプログラムなので,コードはまだ公開できないが,さらに便利なものだ.
このプログラムは,次のような仕様である.
このように欲張って作ってみたが,機能が増えすぎてデバッグする元気がなくなってしまっているため未完成である.いずれはこのプログラムについてもソースを公開して解説をしたいが,今回はご勘弁願いたい.
このアプリケーションは,主たる目的が「ビジュアルなファイルの整理」であり,複数ファイルの一覧性が重要なポイントとなっている.このため,メインウィンドウでの作業が主となるために,「Windowsらしい」方向性で正解なのである.
| 最適なインターフェイスを考えよう |
|---|
このように,「Windowsらしさ」をひとつのキーワードにして,ピクチャービュアーを実現するためのさまざまな定石を紹介してきた.こういった定石は実はコントロールごとにもっとたくさんあるわけなので,これでおしまいということではない.折りにふれて別の定石もご紹介していきたいと思う.
今回のテーマは定石のハズだったが,どうもインターフェイスをどう考えるべきかという方向に向いてしまった.それというのも,インターフェイスはMicrosoftが規定しているにも関わらず,どんどん変更されてしまうし,その方向性が必ずしもよくないと思われるからだ.逆に,規定されていないようなモノであっても,目的によっては使い勝手は良くなる.これをどう考えるべきであろうか.
私には,これが現状のWindowsのインターフェイスの限界,あるいはGUI,OOUIの限界を示すひとつの事象であるように思える.Windowsは最良の解ではないし,目的に応じてもっといろいろな道具が出てくるべきである.あるいは,すでに過去のモノと思われていたCUIについても見直す必要があるだろう.
皮肉なのは,Visual BasicというWindowsでもっとも普及したツールが,そのインターフェイスデザインのイージーさゆえに,あまり標準的でないこともカンタンにできてしまうことだ.だが,これは歓迎すべきことでもある.私たちは,使いやすいものを作ろうとすることができるのだ.もっとも,Visual Basicのバグ対策やWindowsとの整合性が取れないといった部分で余計な時間や労力を使うことはムダだから,やめておいた方がよいだろう.