この Blog で過去にがりがり書いていた、DataGridView で伝票レイアウトを実現するために色々やっていた件について、ありがたいことに今でも見てくれている方がいらっしゃいまして。
サンプルソースが見たいというご要望を頂きましたので、それを公開します。
(ただし DataGridView に関連する部分に限定しているのと、古いソースなので動作チェックしていないのは注意してください)
DL はこちらから。
(http://1drv.ms/1GbzBBB)
DataGridView や DataRepeater にこだわりつつ
時代は Workflow Foundation などと言い続けていたら
これからは LogicFlow だろ! と思い始めてきた
色々浅く広くやっていく古い技術者の Blog
この Blog で過去にがりがり書いていた、DataGridView で伝票レイアウトを実現するために色々やっていた件について、ありがたいことに今でも見てくれている方がいらっしゃいまして。
サンプルソースが見たいというご要望を頂きましたので、それを公開します。
(ただし DataGridView に関連する部分に限定しているのと、古いソースなので動作チェックしていないのは注意してください)
DL はこちらから。
(http://1drv.ms/1GbzBBB)
過去にもちろちろ書いておいたんだけど、修正が発覚したのでw
DataGridViewでアンバウンド時にソートしようと思うと、標準機能だけでなくSortメソッドを利用したソートを行う事が必要になるんだよね。んで、その際には実際に判断を下すためのIComparerインターフェースを継承したクラスを用意すると手軽にいけるのよ。
その際、Compareメソッドをオーバーライドして2値の比較結果を算出するようにするんだけど、その際にちょっと考慮する必要がある事が。
AセルとBセルの値においては「値が無い」状態が数種類存在する
ここなんだよねぇ。具体的にはNothingとDBNullなんだけど、Nothingは「比較対象がない」場合と「比較対象のセルに値がない」場合、でDBNullは「比較対象のセルの値がDBNull」という場合。なので実際にはこういったロジックになるんじゃなかろうか。
Dim originalCell As DataGridViewCell = TryCast(originalRow.Cells(sortChild.ColumnIndex), DataGridViewCell)
Dim comparedCell As DataGridViewCell = TryCast(comparedRow.Cells(sortChild.ColumnIndex), DataGridViewCell)If ((originalCell Is Nothing) OrElse (originalCell.Value Is Nothing) OrElse (IsDBNull(originalCell.Value))) AndAlso _
((comparedCell Is Nothing) OrElse (comparedCell.Value Is Nothing) OrElse (IsDBNull(comparedCell))) Then
Continue For ‘セルの値は同じ
End If
多分こんな感じ。ソース中で「セルの値は同じ」場合にContinueしているのは深読みしてくださいw
社内で講習会なぞをやっているので、VB6・VB.Net共に調べたり話したりすることがそれなりにあるんだけど、グリッド系コントロールはどれもこれも基本思想としては一緒な気がしているんだよね。
異なるのはUIデザイン部分が殆どで、大枠としての制御(値の使い方等)はそんなに違いがないように思えるなぁ。DataRepeaterだって、細かい部分は異なっても大枠の制御は似たようなもんだし。
この部分で違う設計が思いつけば意外とイケるコンポーネントにできるかも。
どうもグリッドという物自体に行き詰まりを感じる・・・
調査の為に、素のDataRepeaterを使って明細にDataGridViewを追加してみた。 結果はアウト。
ここから考えるとDataRepeaterというよりもデータバインディングの仕様の感じがするなぁ。今までもっていたイメージは「選択されているレコードに対する子レコード群をリレーションを通じて取得、表示」だと思っていたんだけど、それだとこの状態にはならないよね。
各行のレコードに対する子レコード群を・・・という仕様だったら恐らく望んでいる状態になると思うんだよね。
そうではなくてあくまでもカレント行に対する子レコード群、だから各行に張り付けたDataGridViewでは同じレコードが表示されているんだろうと思った(ついでに同じ動作をするのも恐らくはデータバインディングのせい)。
んー。これはどうしようかなぁ・・・。
感覚としてはバインディングしている値に細工するか、DataGridView側でフィルタ的な動作を持たせてリレーションを使わないバインディングをすれば見た目は同じ感じになりそうなんだよなぁ。
でも何かひっかかっている・・・w
いや・・・なんというかそんな事するな、という感じがしないでもないんだけど、DataRepeaterコントロールのテンプレートにDataGridViewを追加してみてうまいこと使えるかどうかやってみようと色々試してみたんだよね。
データバインディングの設定なのかわからないんだけど、今のところはDataSetに設定したリレーション通りにはDataGridViewへ表示されないんだよなぁ。表示されるのはDataRepeater1件目のデータに関連する子データのみ。また、DataRepeater内部のDataGridViewをクリックすると、すべてのDataRepeater明細行に存在するDataGridViewが同じ挙動を示すのでものすごく怪しい見た目にw
ちなみに今回も普通にバインディングしているわけではなく、実装者にはアンバインドで使っている気持にするような拡張を施しているのでそこが原因という気がしないでもない。というか、ほぼそのあたりに原因があるんだろうけどねぇ。
右クリックで行削除とかそのあたりは何とかなったんだけど・・・DataGridViewが絡むとやっぱり苦労するw
DataGridViewはレスポンス上の問題があると思われているので、元々編集コントロール(EditingControl)は原則使いまわすようになっているんだよね。で、基本これは問題ないんだけど・・・。
このサイトで色々やってきたように、基本セルは同一のクラスとし、プロパティとして実際に利用する編集コントロールを設定させるタイプの場合この使い回しと思い切りぶつかるんだよねぇ。まぁ、編集クラスは同一だから同じセルと判断されるのは致し方ないのは承知w
色々調べたんだけどDataGridViewの中で隠蔽している部分らしく、外からどうこうできることはなかったんだよね。
ただそれだとあまりにもなので、OnCellEnterあたりのメソッドで
「強制的にEditingPanel内のコントロールを削除」。
一応なんとかなっているけど、ものすごく危険な手段なので別の方法がないかなぁ、と模索中。
MultiLayoutなる伝票入力用のセルや編集コントロールを用意するところまでは、これまでの記事にて色々書いてきたのである程度のイメージはついていると思う。
ここまでやってくると次に出てくる問題は、
「セルをクリックした際にその直下のコントロールへとフォーカスしてほしい」
というところにきてしまうんだよねぇ。使ってみると当然そう思うんだけど。
というのもDataGridViewの仕組み上、セルにフォーカスが移るけどその中のフォーカス制御なんてものは当然やるわけがないからなんだよね。いや、もともとここまでの使い方しようなんて酔狂は少ないから当然なんだけどねw
ここまでやる前にサードパーティ製品を探すからw
でもそこは、あえてDataGridViewでがんばってしまうのがこのサイト。
け、決して貧乏だからとかじゃないんだからね!
某社の製品購入したはいいけど遅かったからやめた、なんてこともないんだからね!
くだらないやりとりはおいといて、この問題を解決するにはHitTesと呼ばれるものを利用する必要がある。
まー、DragDropとかでも利用しているんでたいした話じゃーないけどね。
要は、セルがクリックされた際の座標を編集コントロールにも伝えてあげればいい、というだけだから。
ということで具体的なことは次からの記事で書いていきます。
DataGridViewでキー制御をカスタマイズする際に利用するProcess~系統のメソッド。
ちょっとだけ困ったことがあって、Process系のメソッドのうち一部はOverridableになってないんだよね。
まぁそのあたりも含めてまずは使う必要が多いメソッドについて。
主にオーバーライドを行うのはこのあたり。場合によってはRight、LeftだけではなくUp、DownなどのProcess~メソッドもオーバーライドする必要が出てくるね。Right、LeftはOverridableでないので、Shadowで書き換えてしまいましょう。
なので、DataGridViewとしてキャストした場合は書き換えたロジックが走らないので注意がいりますな。
基本的なところはMSDN2でも見てもらうとして、実際に呼ばれるのはProcessDataGridViewKeyメソッドとProcessDialogKeyメソッドの二つ。ここがキー制御の大本だと思っていいと思う。
若干呼び出されるケースが異なるくらいだけど、どちらもやる内容としては似たようなもんです(w
極端に言えば、Enterキーを押したら右のセルに移れ!という場合、ProcessDialogKeyメソッドとProcessDataGridViewKeyメソッドにて、Enterキーをキャッチした時にProcessRightKeyメソッドを呼ぶとか直接セルを移動させるとかしてあげればいいんだよね。
前回までに書いていたTabFocus関連の話は、ProcessRightKeyメソッドとかProcessLeftKeyメソッドの中で利用してあげるのが、結構スマートかな、なんて思います。
・・・当然もっともっとスマートな方法はあると思うけど。
前回の続きでTabStop制御について。
前回で「何をもってフォーカス可能と判断するか」というのを決めたので、次にやることへ。
というのが次に行う処理。具体的には2方向に分かれることになる。
まぁこれは当たり前なところで、実際には「前か後ろか」の違いがあるだけ。やることは同じ。
そしてここで注意するのは「次ってどっち?」というところ。
画面の仕様にも関連するけれども、次というのが、「右方向」な場合と「下方向」な場合があるよね。
まぁこれはとりあえず右方向用メソッドと、下方向用メソッドで切り分けておいていいと思う。
後々リファクタリングの段階で、くっつけたりまぜたり分解したりしてしまえばいいかな、と。
''' <summary>同じ行の中で次にフォーカス可能なセルを取得する</summary>
Private Function GetAfterCellCanFocusInRow() As System.Windows.Forms.DataGridViewCell
If Me.CurrentCell Is Nothing Then Return Nothing
Dim checkResult As System.Windows.Forms.DataGridViewCell = Nothing
Dim owningCellIndex As Integer = Me.CurrentRow.Cells.IndexOf(Me.CurrentCell)
For Each gridCell As System.Windows.Forms.DataGridViewCell In Me.CurrentRow.Cells
If Not Me.CheckFocusCell(gridCell) Then Continue For
Dim targetCellIndex As Integer = Me.CurrentRow.Cells.IndexOf(gridCell)
If owningCellIndex >= targetCellIndex Then Continue For
checkResult = gridCell
Exit For
Next
Return checkResult
End Function
やろうとするとこんな感じかな?これはサンプルとして「右方向」に走査するメソッドなので、コレを元にして下方向や左方向を操作するメソッドを用意してあげればいいと思う。そこまでできれば、後はProcess系メソッドのオーバーライドへといけるね。
まずTabStop対応を行うに当たって、決めておかないといけないのが。
という点。簡単に言えば「入力可能か?」ということに落ち着きそうなんだけど、それを色々なプロパティを元に判断してあげる必要があるんだよね。今回は(手軽にw)こういう条件で。
それだけの判断で対応したいと思う。実際に業務とかで利用するには、もう少し条件がいるとは思うけどね。
読み取り専用であってもフォーカスさせたいとか、そういうケースはあるだろうから。
まぁ、そのあたりの制御をプロパティなりを通して設定できるように設計するのがベターだと。
なのでまずは、あるセルがこの条件を満たすか、というメソッドを用意してあげよう。
''' <summary>セルがフォーカス可能かをチェックする</summary>
''' <param name="targetCell">対象となるセル</param>
''' <returns>フォーカス可能な際はTrue</returns>
Protected Overridable Overloads Function CheckFocusCell( _
ByVal targetCell As System.Windows.Forms.DataGridViewCell) As Boolean
If (Not targetCell.ReadOnly) AndAlso (targetCell.Visible) Then Return True
Return False
End Function
ざっくり用意するとこんな感じ。これでまずは「フォーカス可能なセルか」という判断ができるようになった。
あとは、次にフォーカス可能なセル、というのを検索する際にこのメソッドをバシバシ利用してあげればいいというハナシ。
その際にどういう処理になるか、ってのは次の記事で。
これもけっこうみかけるサンプルとして、セルを拡張してTabStopを有効化しよう、というのがあるよね。
でも個人的にはこの方針はあまり好きじゃなかったりする。
というのも、セルを拡張して対応するとそのセル以外では利用できなくなるからなんだよね。
普通にDataGridViewTextCellとか利用しても、TabStopが有効にならないから。
なので個人的には、セル拡張もアリだけどDataGridView本体の拡張も行うべきだと思ってる。
実際の対応内容は次回以降だけど、基本方針だけを。
こんなところかな。次回から少しずつこのネタで引っ張る予定w
たぶんDataGridViewを触りはじめでよくひっかかるであろう話題がコレ。
プログラムからは、DataGridView.Rows(Y).Cells(X).Value = "なにがし"とかやっているのに、
画面上では全く反映されていない、というヤツ。
これもわかってしまうと簡単で・・・。
「今現在アクティブなのが編集コントロールだから」
なんだよね。
つまるところ、セルの値は変更されているんだけど編集コントロール側の値が変更されていないので、画面上ではなにも変更されていないように見えるわけで。
んじゃあどうするか、と言われると・・・セルの値が変更された際は編集コントロールの値も変更させればOKなわけで。
If (Me.IsCurrentCellInEditMode) AndAlso _
(e.RowIndex = Me.CurrentCell.RowIndex) AndAlso _
(e.ColumnIndex = Me.CurrentCell.ColumnIndex) Then
If Me.EditingControl IsNot Nothing Then
With DirectCast(Me.EditingControl, System.Windows.Forms.IDataGridViewEditingControl)
If Me.CurrentCell.Value IsNot Nothing Then
.EditingControlFormattedValue = Me.CurrentCell.Value
End If
End With
End If
End If
CellValueChangedイベントとか、OnCellValueChangedメソッドでこんな風にしてあると、どこかからCellの値を操作したタイミングで編集コントロール側の値(EditingControlFormattedValueプロパティ)を書き換えます。ただし、上のサンプルはまだまだ考慮しないといけない点が抜けているので、そのあたりは手を加える必要がありますねぇ。
Null(Nothing)だったらどうするか、とかそういった点は、あえて実装していないということで言い張っていますw
これも色々とサンプルが見当たるものなので、ちょっと注意点を。
RowPostPaintイベントを利用して番号をAPIで直接描写、というのが一番ベターな方法になるのは間違いないところで。
というのも、これと似たような方法として、「RowHeaderCell.Valueに行番号となる値を設定する」方法もあるんだけど、これの場合は「最描写」を意識しないといけないため、ちょっと手間が増えるんだよね。
そしてもうひとつやってはいけないタイミングが存在していて。
RowPostPaintイベントやRowPrePaintイベントにてRowHeaderCell.Valueの値を操作すると、DataGridViewの動作が不安定になります。
というのがあるので、サンプル通り、RowPostPaintイベント(またはOnRowPostPaintメソッド)にてAPIにて番号描写、というのが一番ベターだったりします。
なんで不安定になるか、というところまでは調査していないですけどね。大体は描写しようとしているイベントの中で、その元ネタを操作するからだとは思います・・・。
よくあるネタとして、なんだけどひとつだけ注意点があったので書いておこうかと。
オートコンプリート系のプロパティを開放するようにCell、Columnを継承して拡張するというのは、簡単にできると思う。
ネット上でもIMEModeが設定できるように~とか、その類とやることは同じ。なのでAutoCompleteMode、AutoCompleteSource、AutoCompleteCustomSourceプロパティをそれぞれ開放してあげるだけで、下ごしらえとしてはOKだね。
注意しないといけないのは、そのプロパティをセットするタイミングで。
Enterイベント系(今回はOnEnterメソッド内)でこれらのプロパティをいじくると。
Win32Exceptionが発生します。
MSDNとか調べたんだけど、このあたりについてはどうも記述がのっていなかったんだよねぇ・・・。
まぁ理由はわからずともそうなってしまうので(w)、メソッドの本来の目的通りCellクラスのInitializeEditingControlメソッドで設定してあげるのが一番いいんじゃないかな?
以前までで色々書いてきたMultiLayoutと呼んでいるやり方を使うとこんなこともできたり。
こんな状態の表示が・・・
こんな風に切り替えられます。しかも特にロジックは必要なく。
方法としてはwebブラウザで利用するCSSを切り替える、みたいなやり方なので楽にできるんだよねぇ。
前回ダイアログを表示するときにはちょっとロジックがいる、というのを書いた。
簡単に言うとこういったことをやる必要があるってとこで。
言葉で書くと2行なんだけどねw
実際にはこんな感じで表示座標を算出しないといけないのよ
Dim editCell As SubEditorDialogCell = TryCast(Me._parentDataGridView.CurrentCell, SubEditorDialogCell)
If (editCell Is Nothing) OrElse (editCell.EditorInstanceType Is Nothing) Then Return
subEditorInstance = TryCast(System.Activator.CreateInstance(editCell.EditorInstanceType), SubEditorDialog)
If subEditorInstance Is Nothing Then Return
'位置の割り出し
Dim cellRectangle As System.Drawing.Rectangle = editCell.DataGridView.GetCellDisplayRectangle(editCell.ColumnIndex, editCell.RowIndex, True)
Dim cellPoint As System.Drawing.Point = Me._parentDataGridView.PointToScreen(New System.Drawing.Point(cellRectangle.Left, cellRectangle.Top))
'表示位置調整
Dim editorSize As System.Drawing.Size = subEditorInstance.Size
Dim owner As System.Windows.Forms.ContainerControl = DirectCast(Me._parentDataGridView.TopLevelControl, System.Windows.Forms.ContainerControl)
Dim ownerSize As System.Drawing.Size = owner.ClientRectangle.Size
If (owner.Left + ownerSize.Width) < (cellPoint.X + editorSize.Width) Then cellPoint.X = owner.Left + ownerSize.Width - editorSize.Width
If (owner.Top + ownerSize.Height) < (cellPoint.Y + editorSize.Height) Then cellPoint.Y = owner.Top + ownerSize.Height - editorSize.Height
Dim afterCellPoint As System.Drawing.Point = Me._parentDataGridView.TopLevelControl.PointToClient(cellPoint)
ポイントとなるのは「親コンテナのクライアント領域」。これをコンテナのSizeとかからやると、コンテナにスクロールバーがでたり一部未表示になったりするか注意が必要。
まあこんな感じで表示位置を求めてあげれば、あとはLocationプロパティとかにつっこむことによってちゃんと表示されるって寸法です。
伝票レイアウトセル(2) 編集コントロール その5で触っている、編集開始時の処理。
このときはDataGridViewのEditingPanelに対して直接コントロールを追加することによって、UserControlを継承した編集コントロールを利用するようにしていたんだけど。
ダイアログで行う場合は、当然この方法だとマズイ場面が。
・・・それはダイアログのサイズ。
ダイアログで、となるとケースによっては元のセルよりも大きいものを利用する場面というのが当然でてくるわけで。
もっといってしまうと、親元のDataGridViewよりも大きいケースというのがありえるわけで。USのMSDNフォーラムにあるサンプルは「セルをダイアログの大きさに拡張する」という手法を使っているけど、それだとちとまずい。
(本当につきつめると親Formよりも・・・というのがあるんだよねぇ)
まぁそういうところがあるのでちょっと工夫する必要があるんだけど、今回は「親Formよりは小さいけどDataGridViewよりは大きいかも知れない」というケースまで対応する場合にどうするか、でいこうと思う。
んで、実は答えはすこぶる簡単。
Me.EditingControlDataGridView.TopLevelControl.Controls.Add(subEditorInstance)これだけ。前はEditingPanelにAddしていたのを、「親DataGridViewの所属するTopLevelControl」に変更してあげればほとんどいける。実際は「表示する位置」の問題があるので、もう少しロジックはいるけどね。それは次回以降で。
ちなみにこの場合、AddするコントロールはTopLevel=FalseにしておかないとAddできないので注意。
今までに色々やってきた方法は、DataGridViewの中に伝票を埋め込んでしまおうという見た目にわかりいい方法だったんだけど、単純に入力だけできればいい場合には別の方法もあるよねぇ。
それはダイアログ形式でエディタを表示するという方法。
セルの編集状態が開始した際に同じ場所にダイアログを表示して入力させるというやりかたなんだけど、この方法を利用した場合の問題点は何個かあるんだよね。
ちょっと今回から何回か続けてこのやり方の対応を書いていこうと思う。
今までのMultiLayoutとよばれるやり方に比べると、わかりやすいから需要があるしね。それにMultiLayoutをちょっと変更してあげれば対応できてしまうんで、実はそれほど難しい話題じゃないしw
せいぜい気をつけないといけないポイントが何箇所かあるくらいかな?
なんというかMSDNフォーラムの方で引き合いに出していただけて嬉しいやら恥ずかしいやら。
ただ自分ができているのは「セルの拡張」だけなので、「ヘッダの拡張」というのはちゃんとできてないんだよねぇ。
というのも純粋にヘッダーを表示するまでならセルの拡張と話は同じなので、どうにでもできるんだけど。
デザインのさせ方
こればっかりがまったくアイデアも何もない。セルと同様にUserControlを継承したクラスで・・・とすれば確かに表示まではどうにかなります。
でもそれだとデザインがえらいやりにくいんですよね。
ヘッダをデザインするときは、普通ディティール部のデザインとあわせてやることが多いだろうし。
そうじゃないと几帳面なところではつっかえされてしまいまするよ。
ExpressEdition以外であれば、デザイナ自体を作成して・・・という方向にいけるんだろうけど、何しろ間違った方向へと突き進むこのサイトは、原則ExpressEditionが対象w
ExpressEditionでできないことはやらない、という"( ゚,_ゝ゚)バカジャネーノ"、という意気込みなんですね。
そうなると考え付く対応ってのもかなり限られてしまい。
ぐらいなんだよなぁ・・・。手間の量からいくと「1」がまだマシ。使いやすさという点で「2」がベター。
どちらにしろ手間はかかりますね。このあたりまでくると、どうしても市販コンポーネントを採用する方へと流れてしまいます。