- PR -

ListViewの背景色を変更した時のちらつきを防ぎたい

投稿者投稿内容
kamada
会議室デビュー日: 2005/09/14
投稿数: 6
投稿日時: 2005-09-14 17:18
現在以下のような機能の実装を検討しています。

(1) まず ListView に初期データを投入
(2) データが更新されたら該当するセルのテキストを新データに差し替え
(3) データが更新されたことを示すため、一定時間そのセルの背景色を変更する。

(1)と(2)だけに限定すると、データ更新のあった ROW だけがちらつくので、テキストの
差し替えだけなら ROW 単位の再描画が行われるのではないかと思います。

ところが(1)から(3)までを行うと、ListView 全体がちらつきます。ですのでセルの背景色を
変更すると ListView 全体が再描画されているような気がします。

ここで、理想としてはちらつきを完全に抑えたく、それが無理ならセル単位、最低でも
ROW 単位でのちらつきに抑えたいのですが、何か良い方法はありますでしょうか。

なお ListView はデータの表形式での表示をさせる上で最も軽量なコントロールだと思って
使用していますが、他に良いコントロールがあればそちらを選択するのに吝かではありません。

よろしくお願い致します。
ya
大ベテラン
会議室デビュー日: 2002/05/03
投稿数: 212
投稿日時: 2005-09-14 17:47
ListView.BeginUpdate,EndUpdateメソッドの実行をきちんとしていますか?


[ メッセージ編集済み 編集者: ya 編集日時 2005-09-14 17:49 ]
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2005-09-14 19:13
取りあえず思いついた方法。
  1. ListViewを継承したコントロールを作成。
  2. 背景を書き換えるのに独自メソッド(仮称:ChangeSubItemBackColor)を使う。
  3. ChangeSubItemBackColorはrowIndex, columnIndex, backColorを引数に取る。
  4. ChangeSubItemBackColorでは、変更するListViewItemからそのBoundsを取得し、フィールド(仮称:changedBounds)に保存する。
    • .NET 2.0を使うかLVM_GETSUBITEMRECTメッセージを送るかすれば、サブアイテムのBoundsも取得できる。この場合先頭カラムはListViewItem.Boundsと同じ範囲を示すので注意が必要。
  5. WndProcをオーバーライドし、WM_PAINTの時にchangedBoundsがRectangle.Emptyで無い場合に、
    1. changedBoundsはローカル変数boundsにコピーしてからRectangle.Emptyにリセット。
    2. Win32APIのValidateRect関数を、自身のBoundsの範囲を指定して呼び出して、更新リージョンから削除する。
    3. Invalidateメソッドでboundsだけ再描画するように指示。


こんなのでどうでしょう。
kamada
会議室デビュー日: 2005/09/14
投稿数: 6
投稿日時: 2005-09-15 10:31
引用:

yaさんの書き込み (2005-09-14 17:47) より:
ListView.BeginUpdate,EndUpdateメソッドの実行をきちんとしていますか?




実行していますが、あまり効果がないようでした。


[ メッセージ編集済み 編集者: kamada 編集日時 2005-09-15 10:31 ]
kamada
会議室デビュー日: 2005/09/14
投稿数: 6
投稿日時: 2005-09-15 10:34
引用:

Hongliangさんの書き込み (2005-09-14 19:13) より:
取りあえず思いついた方法。
  1. ListViewを継承したコントロールを作成。
  2. 背景を書き換えるのに独自メソッド(仮称:ChangeSubItemBackColor)を使う。
  3. ChangeSubItemBackColorはrowIndex, columnIndex, backColorを引数に取る。
  4. ChangeSubItemBackColorでは、変更するListViewItemからそのBoundsを取得し、フィールド(仮称:changedBounds)に保存する。
    • .NET 2.0を使うかLVM_GETSUBITEMRECTメッセージを送るかすれば、サブアイテムのBoundsも取得できる。この場合先頭カラムはListViewItem.Boundsと同じ範囲を示すので注意が必要。
  5. WndProcをオーバーライドし、WM_PAINTの時にchangedBoundsがRectangle.Emptyで無い場合に、
    1. changedBoundsはローカル変数boundsにコピーしてからRectangle.Emptyにリセット。
    2. Win32APIのValidateRect関数を、自身のBoundsの範囲を指定して呼び出して、更新リージョンから削除する。
    3. Invalidateメソッドでboundsだけ再描画するように指示。


こんなのでどうでしょう。



内容が私には難しいので、しばらく吸収させて下さい。
試してみて結果を後ほどご報告します。
kamada
会議室デビュー日: 2005/09/14
投稿数: 6
投稿日時: 2005-09-22 13:08
Hongliangさんのアイデアを元に実装してみました。

環境は.NET 1.1です。
ひとまず実装の簡単そうなRow単位の再描画を試しました。

このリストビューは比較的高い頻度で(秒10回以上)データの更新が起こるのですが、
この実装を入れることにより、ある程度まではRow単位の再描画で済むようなっている
ようです。

ただし、約10秒に1度程度のタイミングでList全体の再描画がおきてしまうようです。
(全体再描画のタイミングは一定ではありません)

参考までにこちらで書いたコードを載せますので、なにか指摘があればお願いします。

//
//チラつかないListView
//
public class NonFlickerListView : System.Windows.Forms.ListView
{
private Rectangle changedBounds = Rectangle.Empty;
private const int WM_PAINT = 0x000F;

public void ChangeSubItemBackColor(int rowIndex,int columnIndex,Color backColor)
{
//行全体のBoundsを保持
changedBounds = this.Items[rowIndex].Bounds;
//背景色を設定
this.Items[rowIndex].SubItems[columnIndex].BackColor = backColor;
}

protected override void WndProc(ref Message m)
{
   if(m.Msg == WM_PAINT && changedBounds != Rectangle.Empty)
{
Rectangle bounds = changedBounds;
changedBounds = Rectangle.Empty;
ValidateRect(m.HWnd, bounds);
Invalidate(bounds);
}
    else
{
base.WndProc(ref m);
}

}

  // Win32 APIのインポート
  [DllImport( "user32.dll", SetLastError = true )]
  extern static bool ValidateRect(IntPtr hWnd,Rectangle lpRect);
}

//
//フォーム
//
public class Form1 : Form
{
NonFlickerListView myListView;

//更新時の処理
void Update()
{
.....
myListView.ChangeSubItemBackColor(行,列,Color.Red);
}
}
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2005-09-22 17:08
んー、コードを見てちょっと。
  • ValidateRectは、指定した四角形を再描画の対象から外す関数です。boundsは再描画する必要がある範囲ですから、これを引数にしては元の木阿弥です。渡すのは画面全体です。
  • そもそもValidateRectの第二引数はRECT構造体へのポインタです。
    • System.Drawing.Rectangleをildasmで覗くと分かりますが、フィールドはInt32のheight、width、x、yで構成されています(更に詳しく言えば、x、y、width、heightの順です)。しかしRECT構造体はLONG(32bit)のleft、top、right、bottomで構成されています。サイズは同じですが、全く別の構造です。
    • ポインタを受け取るのですから、直接値型を渡してはいけません。参照を渡す必要があります。そういう場合はrefキーワードを使います。
    • ValidateRect関数の解説を良く読むと、lpRectの解説に
      引用:
      NULL を指定すると、更新リージョンからクライアント領域全体が削除されます(クライアント領域全体が有効化します)。

      と書かれています。つまりNULLポインタを渡せば画面全体をあらわすRECT構造体を渡したのと同じになります。NULLポインタとは、即ちIntPtr.Zeroです。
    • 一見うまくいっているのは、ポインタを渡すはずが実際の値を渡してしまっていて、それが恐らく0であったために(ListViewItem.Bounds.Xは普通0ですからね)全体が再描画の対象から外れたのでしょう。ですから、左右のスクロールバーをちょいと右にずらしてListViewItem.Bounds.Xを0以外にしてやると、途端に全行再描画され出します。
  • しかし全体の再描画は試してみても発生しませんが。上で言ったように、何らかの原因でListViewItem.Bounds.Xが0以外を指したためではないでしょうか?


サブアイテム単位での再描画を行うコードを書いてみました。参考にしてみてください。
なお#if !V10 && !V11 から #elseまでは.NET 2.0用のコードなので削除しておいてください(あと#endifの行も)。
kamada
会議室デビュー日: 2005/09/14
投稿数: 6
投稿日時: 2005-09-22 17:52
ご指摘ありがとうございます。

連休中に指摘事項を吸収させて頂いて、
休み明けに結果をご報告したいと思います。

スキルアップ/キャリアアップ(JOB@IT)