はじめまして。
Delphi&Windowsプログラミング歴10日目で右も左も分かりませんが、よろしくお願いします。
TListViewのvsReport形式を使い、CheckBoxesをTrueにしています。
チェックボックスを「チェックなし」、「チェックあり」、「チェック不可(グレー色)」に
したいと考えました。
サンプルを探しても見つからなかったので、難しい方法しかないのかとあきらめかけてましたが、
ImageListをStateImagesに設定し、StateIndexを操作すれば可能なことを知り、やってみました。
表示された自作画像のチェックボックスをクリックすると、チェックなし→ありと変化して、
成功したように見えたんですが…大問題が。クリックしても、チェックの状態変化がおかしいのです。
正常なら Item.Checked=False、True、False、True、…と変化するはずですが、
なぜか False、True、True、True、False、True、True、True、False、True、…となります。
表示だけの問題か、チェックに制限を設けている部分の問題かと思いましたが、分かりませんでした。
そもそも無理な使い方をしているのでしょうか。でも一応クリックに反応はするんですよね…。
以下がソースの抜粋です。実際には他の処理も入ってます。
Const
ResultSII=3; // ステータスを格納するSubItemのIndex
M_kanou='可能'; // ステータスの各表示内容
M_huka= '不可'; //
procedure TForm1.ListView1CustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);
begin
// SubItemの数がResultSIIまで達していないItemはグレーに
if Item.SubItems.Count<ResultSII+1 then
begin
Item.StateIndex:=2;
exit;
end;
// SubItems[ResultSII]が「可能」のItemはStateIndexをCheckedに合わせ、それ以外はグレーに
if Item.SubItems[ResultSII]=M_kanou then
if Item.Checked then Item.StateIndex:=1
else Item.StateIndex:=0
else
Item.StateIndex:=2;
end;
procedure TForm1.ListView1Change(Sender: TObject; Item: TListItem;
Change: TItemChange);
begin
// SubItemの数がResultSIIまで達していないItemは無条件でChecked:=False
if Item.SubItems.Count<ResultSII+1 then Item.Checked:=False
else
//SubItems[ResultSII]が「可能」以外のItemは、Checked=Trueにさせない
if Item.SubItems[ResultSII]<>M_kanou then Item.Checked:=False;
end;
TListViewのvsReport形式でチェックボックスの状態を
・ 「チェックなし」、「チェックあり」、「チェック不可(グレー色)」 または、
・ 「チェックなし」、「チェックあり」、「チェック不可(グレー色)」、「チェック強制(グレー色)」
のようにしたいという需要は結構あると思うのですが、検索した限りでは見つかりませんでした。
どのようにすれば良いのか、教えていただければ幸いです。
提示されたソースではListViewが酷くちらついて使い物にならないけど。
SubItems[ResultSII]の「可能」と「不可」の切り替えはどうやってるの?
>そもそもさん
いえ、ひどくちらつくということは無いです。CPUが1.2GHzの古PCだからでしょうか。
それとも、まだ項目(=Itemでいいんですよね?)を仮に8個程度しか入れてないからかな?
なお、仮の項目はIDEで手入力しました。項目数は最大で50個にするつもりです。
ステップ実行等してみると何回か描画してるようで、クリックされた項目だけは2〜3回描画を
してました。
目を凝らすと確かに、クリックされた項目だけはその瞬間ちらついているように感じます。
正しくはどういう作りにするべきなのでしょうか?
「可能」と「不可」の切り替えですが、そういう部分はまだ作っていません。
「可能」は、あるメインの処理が終わったら「完了」に切り替えるつもりです。
「不可」や「完了」は変化しません。
ListViewのチェックボックスの状態は、StateIndexを使って管理されています。
(MSDNによると1がUncheckedで2がCheckedだとか)
さらに、VCLもCheckedとStateIndex値の値を別々にキャッシュしているので、話がややこしくなっています。
結局、それぞれが干渉し合って正しく状態が更新されないようですね。
いっそCheckBoxesは外して、StateIndexだけ使って自前で管理するのが良いのではないでしょうか。
ついでに今後問題になりそうなところ:
・OnCustomDrawItemは、状態が変わった「結果」として再描画のため呼び出されるものなので、そこで状態を変えるべきではありません。
・Changedはリスト項目の状態が変わるとその都度呼び出されます(例えば、リストの選択を変更するだけでも数回続けて呼ばれます)。
なので、SubItemとかCheckedが変更された時だけ呼び出されると仮定するのは危険。
また、その内部でCheckedなどの状態を変更すると、無限呼び出しに陥る可能性があります。
初めて使ってみましたが、確かに挙動不審ですね。
こんな感じでどうでしょう?
procedure TForm1.FormCreate(Sender: TObject);
var
i,p: Integer;
str:string;
item:TListItem;
begin
ListView1.Checkboxes:=True;
for i := 0 to 10 - 1 do
begin
item := ListView1.Items.Add;
p := i mod 4;
case p of
0:str:='未チェック';
1:str:='チェック';
2:str:='絶対未チェック';
3:str:='絶対チェック';
end;
item.Caption := str + IntToStr(i);
Item.StateIndex := p;
end;
end;
procedure TForm1.ListView1AdvancedCustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; Stage: TCustomDrawStage;
var DefaultDraw: Boolean);
begin
if item.StateIndex >= 2 then
ListView1.Canvas.Font.Color := clRed;
end;
procedure TForm1.ListView1ItemChecked(Sender: TObject; Item: TListItem);
begin
if item=nil then
Exit;
if item.StateIndex >= 2 then
begin
item.Checked := not Item.Checked;
item.Checked := (item.StateIndex mod 2 = 1);
end;
end;
>torさん、アドバイスありがとうございます!
StateIndexで管理してるとは思いもよりませんでした。生のListViewコントロールでは、
という話ですよね。
でもそれで不都合が発生するのって、もしかしてVCLにバグがあるんじゃ…。
自前で管理となると、OnClick、OnMouseDownとか使うのですよね。何を使えば良いのでしょう?
こちらの手法などが応用できるのでしょうか。
https://www.petitmonte.com/bbs/answers?question_id=5651
今後問題になりそうなところ...ですが、様々な場合に呼び出されるであろうことは想定してました。
が、Checkedの変化によって呼び出されるイベントの内部でCheckedの状態を変えるのは危険なのですね。
例えばですが、「特定のチェックボックスの状態は変化させない」という条件で、上記のコードの
OnChange部分をOnChangingに移し、AllowChangeパラメータをFalseにするのはセーフなのでしょうか。
なお、余計な内容を除去した下記のコードで実験を繰り返しました。
ListView、Memo、Timer、チェックボックス画像を入れたImageListを配置してます。
その成果は…間違い無くCheckedの状態変化がおかしいことが判明しただけでした(涙)
var
ItemChkd: Boolean;
StateIdx:Integer;
procedure TForm1.FormCreate(Sender: TObject);
begin
Timer1.Enabled:=False; //タイマーを止めておく
Memo1.ScrollBars:=ssBoth;
ListView1.ViewStyle:=vsReport; //ListViewの初期化
ListView1.Columns.Add;
ListView1.Checkboxes:=True;
ListView1.ReadOnly:=True;
ListView1.StateImages:=ImageList1;
ListView1.Items.Add.Caption:='テスト'; //テスト用に1個だけItemを置く
ListView1.Items[0].StateIndex:=0; //0=チェックON画像、1=チェックOFF画像
ItemChkd:=not ListView1.Items[0].Checked; //Checkedの変化検出用
StateIdx:=19; //StateIndexの変化検出用
Timer1.Interval:=1; //タイマー始動
Timer1.Enabled:=True;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
LIC:Boolean;
LIS:Integer;
begin
LIC:=ListView1.Items[0].Checked;
LIS:=ListView1.Items[0].StateIndex;
if (ItemChkd<>LIC) or (StateIdx<>LIS) then
begin
Memo1.Lines.Add(IfThen(LIC,'True ','False ')+IntToStr(LIS));
ItemChkd:=LIC;
StateIdx:=LIS;
end;
end;
procedure TForm1.ListView1Changing(Sender: TObject; Item: TListItem;
Change: TItemChange; var AllowChange: Boolean);
var
StrC:String;
begin
case Change of
ctText: StrC:='ctText';
ctImage: StrC:='ctImage';
ctState: StrC:='ctState';
end;
Memo1.Lines.Add('Changing!! '+StrC);
end;
end.
>ImageListをStateImagesに設定し、StateIndexを操作すれば可能なことを知り、やってみました。
>表示された自作画像のチェックボックスをクリックすると、チェックなし→ありと変化...
StateImagesに指定したImageListの中の自作画像が以下のようになっているとして、
0番目:チェックなし
1番目:チェックあり
2番目:グレー(チェックなし)
3番目:グレー(チェックあり)
自前の画像でチェックボックスを表示する場合、Checkboxesプロパティは Falseにして、以下のコードでチェックを切り換えればいい。
なお、Checkedプロパティはチェックの有無の判定には使えないので、StateIndexの値で判定する。
type
TListView = class(ComCtrls.TListView)
private
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
end;
type
TForm1 = class(TForm)
ListView1: TListView;
......
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
const
ResultSII = 3;
M_Kanou = '可能'; // チェック ON/OFF変更可
M_Fuka = '不可'; // グレー (ON/OFFはそのまま)
// チェックのON/OFF
procedure TListView.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
Item: TListItem;
begin
inherited;
Item := GetItemAt(X, Y);
if Item = nil then exit;
if Item.SubItems[ResultSII] = M_Kanou then begin
if htOnStateIcon in GetHitTestInfoAt(X, Y) then begin
if Item.StateIndex = -1 then Item.StateIndex := 0;
Item.StateIndex := 1 - Item.StateIndex;
end;
end;
end;
※ 各ItemのStateIndexは、項目設定時にあらかじめセットしておくこと。
※「可能」から「不可」にするには、Item.StateIndex := Item.StateIndex or 2;
※「不可」から「可能」にするには、Item.StateIndex := Item.StateIndex and 1;
上記のリスト中、私にとって肝心な所のコメント部分を間違えました。正しくは、
ListView1.Items[0].StateIndex:=0; //0=チェックOFF画像、1=チェックON画像
です。
あららっ! ちょっと書き込みがかぶってました。
そもそもさんですね!!
サンプルコードをありがとうございます!!!
しかし、現時点での私の理解を超える内容のような気がしますので、しばらく研究させていただいて
からコメントいたします。
>monaaさん、サンプルコードまで書いていただいて、ありがとうございます!
挙動不審なことを理解していただいてうれしいです。一人で悩んでいたものですから。
ObjectPascalプログラムを構成する要素がまるで理解できていないので、一行一行が参考になります。
ただ…これはStateIndexの内容によりCheckedの状態変化に制限を加え、さらに項目の色も変える、
というサンプルですよね??
すみません、確かにプログラム中のどこでCheckedの状態変化を制限するのかという課題もあるのですが、
3状態のチェックボックスをどうやってうまく表示させるか→どうやってチェックボックスがクリック
されたことを検出するのか、というのが当面の課題になってきております…。
あの、その、、、後出し環境で大変申し訳無いのですが、Delphi 6 Personalを使用しておりまして、
OnItemCheckedというイベントが見当たらないのです…。
調べた限りでは、Delphi 2009からの新機能ではないかと(ググっても情報が少ないです)。
☆Delphi 2009 および C++Builder 2009 の新機能 - RAD Studio(共通)
http://pdf.openvista.jp/view/medium/p76-77/http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate1/JA/pdf/devcommon.pdf
※2ページ目にOnItemCheckedについて載ってます
せっかく教えていただいたのに活用できなくてすみません。
でもOnItemCheckedイベント、気になります。チェックボックスのクリックで呼び出され、その中では
Checkedの変更をしてOkですよ、というものでしょうか?
あと、Checkedの状態変化をStateIndexが0,1なら制限無し、2なら常時OFF、3なら常時ONとするならば、
item.Checked := not Item.Checked; ←下の行で決まってしまうような??
item.Checked := (item.StateIndex mod 2 = 1); ←(item.StateIndex = 3); ではダメですか?
もしくは、
item.Checked := not Item.Checked;
if item.StateIndex >= 2 then
item.Checked := (item.StateIndex mod 2 = 1);
なら、つじつまが合うような気がするのですが…。
monaaさんの意図している動作と、私の求めている動作が違っていたらすみません。
さらにちょっと疑問なんですが、OnCustomDrawでなくOnAdvancedCustomDrawにはどういった利点がある
のでしょうか。
OnCustomDrawのヘルプによると、「…デフォルトの描画処理を他のステージ(背景が消去されたとき,
またはリストビューの描画が完了したときなど)でも実行するには,OnAdvancedCustomDraw イベントを
かわりに使います。」となっていますが、ちんぷんかんぷんです…。
手元にD7以下が無いのでD7で動作確認しました。
これで動作しますかね?
ListViewはOSのバージョンの制約を受けます、
ご自分でチェックイメージを作成するなら、もう少し簡単かもしれません。
今時間が無いのでコメントは読んでません、また後で来ます。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ComCtrls, CommCtrl;
type
TForm1 = class(TForm)
ListView1: TListView;
procedure FormCreate(Sender: TObject);
procedure ListView1AdvancedCustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; Stage: TCustomDrawStage;
var DefaultDraw: Boolean);
private
{ Private 宣言 }
procedure ListViewWindowProcEx(var Message: TMessage) ;
procedure ListView1ItemChecked(Item: TListItem);
public
{ Public 宣言 }
end;
var
Form1: TForm1;
OriginalListViewWindowProc:TWndMethod;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
var
i,p: Integer;
str:string;
item:TListItem;
begin
OriginalListViewWindowProc := ListView1.WindowProc;
ListView1.WindowProc := ListViewWindowProcEx;
ListView1.Checkboxes:=True;
for i := 0 to 10 - 1 do
begin
item := ListView1.Items.Add;
p := i mod 4;
case p of
0:str:='未チェック';
1:str:='チェック';
2:str:='絶対未チェック';
3:str:='絶対チェック';
end;
item.Caption := str + IntToStr(i);
Item.StateIndex := p;
item.Checked := not Item.Checked; //ここがミソ
item.Checked := not Item.Checked; //ここがミソ
end;
end;
procedure TForm1.ListView1ItemChecked(Item: TListItem);
begin
if item=nil then
Exit;
if item.StateIndex >= 2 then
item.Checked := (item.StateIndex mod 2 = 1);
end;
//This code was copyed from...
//http://delphi.about.com/od/delphitips2007/qt/listviewchecked.htm
procedure TForm1.ListViewWindowProcEx(var Message: TMessage) ;
var
listItem : TListItem;
begin
if Message.Msg = CN_NOTIFY then
begin
if PNMHdr(Message.LParam)^.Code = LVN_ITEMCHANGED then
begin
with PNMListView(Message.LParam)^ do
begin
if (uChanged and LVIF_STATE) <> 0 then
begin
if ((uNewState and LVIS_STATEIMAGEMASK) shr 12) <> ((uOldState and LVIS_STATEIMAGEMASK) shr 12) then
begin
listItem := listView1.Items[iItem];
if listItem.StateIndex >= 2 then
ListView1ItemChecked(listItem);
end;
end;
end;
end;
end;
//original ListView message handling
OriginalListViewWindowProc(Message) ;
end;
procedure TForm1.ListView1AdvancedCustomDrawItem(Sender: TCustomListView;
Item: TListItem; State: TCustomDrawState; Stage: TCustomDrawStage;
var DefaultDraw: Boolean);
begin
if item.StateIndex >= 2 then
ListView1.Canvas.Font.Color := clRed;
end;
end.
>monaaさん、素早い切り返しに驚きました。お忙しいところ恐縮です。
まずご報告ですが、動作します!
■StateImages未設定 http://sakuratan.ddo.jp/imgboard/img-box/img20100327224430.png
一番上はクリックしてチェックを入れてみました。「絶対」以外のチェックON/OFF自由です。
> item.Checked := not Item.Checked; //ここがミソ
この部分を1つコメントアウトしてみると、「絶対」以外のチェック状態が反転します。
2つともコメントアウトしてみると、絶対チェックのチェックボックスが消滅します(?)
1回クリックすれば出現します。
■StateImages設定後 http://sakuratan.ddo.jp/imgboard/img-box/img20100327224545.png
残念ながらチェックボックス画像がこの状態のまま変化しません…。
>ご自分でチェックイメージを作成するなら、もう少し簡単かもしれません。
いえ、初めから自作画像を使っております…イメージとは画像のことですよね?
作っていただいたこのサンプルは私の理解をはるかに超えています。
ListViewWindowProcExというのがOnItemCheckedを追加するためというのは想像できるのですが、
・ listView1.Items[iItem]; ←このiItemってどこからやって来たのでしょう??
・ Item.Checkedは初期設定で必ずFalseになるはずなのに、なぜFalseとTrueが交互に並ぶのか??
分からないことだらけです。
コードの一部を変更して所望する動作を得ようと試行錯誤しましたが、例外エラーが出てしまい、
うまく行きませんでした…。
せっかくのサンプルを活かせなくてすみません。
>そもそもさん、ありがとうございました!
動きます! 期待していた動作とバッチリ合います!!
現状はこんな感じです。
http://sakuratan.ddo.jp/imgboard/img-box/img20100328083657.png
当たり前かもしれませんが、もはやCheckboxesプロパティのTrue/Falseには影響されません。
ただ、当初は通常の並びのようにTForm1.FormCreateを頭に、TListView.MouseDownをその下に持って
きてしまい、コンパイルしても何も起きずエラーも出さず、という状態でしたのでかなり悩みました。
functionとかは先に置く必要がある、という記述を思い出し、並び替えたところ動き出しました!
私の変てこりんな「ResultSII」も考慮していただいて、本当に感謝感激です。
少し疑問があります。質問ばかりですみません。
・ TListView.MouseDownの位置はTForm1.FormCreateより前ならば良い、ということでしょうか。
・ overrideという手法(ですよね?)でなく、通常のOnMouseDownに入れることは出来るのでしょうか。
なお、ListViewのこういった使い方は私のような初心者ほど欲してると推測します。
ですので、皆さんからいただいた知恵を有効な資産として残すため、もう少しコードを汎用化してから
サンプルをアップしたいと思います。monaaさんから教えていただいた知恵もすでに盛り込んでます。
なんかこのトピックがプログラムリストだらけになっちゃいますが、いいですよね?
TForm1 のクラス宣言よりも前(上)に TListview のクラス宣言を置くこと。
実現部の TListView.MouseDown と TForm1.FormCreate の位置関係はどうでもかまわない。
OnMouseDownハンドラ と MouseDownのoverrideは、どちらも長所短所があるので、どちらを使うかは場合によりけり、好みの問題とも言えるかな?
>私の変てこりんな「ResultSII」も考慮していただいて、本当に感謝感激です。
個人的には [ResultSII] があまり良くないので...こんなのはどうかなと
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ImgList, ComCtrls, CommCtrl, StdCtrls;
type
TListItem = class(ComCtrls.TListItem)
private
protected
function GetChecked: Boolean;
procedure SetChecked(Value: Boolean);
function GetEnabled: Boolean;
procedure SetEnabled(Value: Boolean);
function GetStateIndex: Integer;
procedure SetStateIndex(Value: Integer);
public
published
property Checked: Boolean read GetChecked write SetChecked;
property Enabled: Boolean read GetEnabled write SetEnabled;
property StateIndex: Integer read GetStateIndex write SetStateIndex;
end;
TListView = class(ComCtrls.TListView);
TForm2 = class(TForm)
ListView1: TListView;
ImageList1: TImageList;
Button1: TButton;
Button2: TButton;
procedure ListView1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
Rect: TRect; State: TOwnerDrawState);
procedure FormCreate(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
function TListItem.GetStateIndex;
begin
Result := inherited StateIndex;
end;
procedure TListItem.SetStateIndex(Value: Integer);
begin
inherited StateIndex := Value;
if Value and 1 = 0 then
begin
inherited Checked := False;
end
else
begin
inherited Checked := True;
end;
end;
function TListItem.GetEnabled: Boolean;
begin
if StateIndex and 2 = 0 then Result := False else Result := True;
end;
procedure TListItem.SetEnabled(Value: Boolean);
begin
if Value = True then
begin
if StateIndex = -1 then
begin
StateIndex := 2;
end
else
begin
StateIndex := StateIndex or 2;
end;
end
else
begin
if StateIndex = -1 then
begin
StateIndex := 0;
end
else
begin
StateIndex := StateIndex and (255-2);
end;
end;
end;
function TListItem.GetChecked: Boolean;
begin
Result := inherited Checked;
end;
procedure TListItem.SetChecked(Value: Boolean);
begin
inherited Checked := Value;
if Checked = True then
begin
if StateIndex = -1 then
begin
StateIndex := 1
end
else
begin
StateIndex := StateIndex or 1;
end;
end
else
begin
if StateIndex = -1 then
begin
StateIndex := 0;
end
else
begin
StateIndex := StateIndex and (255-1);
end;
end;
end;
procedure TForm2.FormCreate(Sender: TObject);
var
ListItem: TListItem;
begin
ListItem := TListItem(ListView1.Items.Add);
ListItem.Caption := 'NO 1';
ListItem.Checked := False;
ListItem.Enabled := False;
ListItem := TListItem(ListView1.Items.Add);
ListItem.Caption := 'NO 2';
ListItem.Checked := True;
ListItem.Enabled := False;
ListItem := TListItem(ListView1.Items.Add);
ListItem.Caption := 'NO 4';
ListItem.Enabled := True;
ListItem.Checked := True;
ListItem := TListItem(ListView1.Items.Add);
ListItem.Caption := 'NO 3';
ListItem.Checked := False;
ListItem.Enabled := True;
ListView1.OwnerDraw := True;
ListView1.ViewStyle := vsReport;
end;
procedure TForm2.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
Rect: TRect; State: TOwnerDrawState);
var
Bitmap: TBitmap;
begin
if Item.Enabled = False then
begin
ListView1.Canvas.Font.Color := clGray;
end;
Bitmap := TBitmap.Create;
Bitmap.PixelFormat := pf32bit;
Bitmap.SetSize(16,16);
ImageList1.GetBitmap(Item.StateIndex,Bitmap);
Sender.Canvas.TextRect(Rect,Rect.Left+20,Rect.Top,Item.Caption);
Sender.Canvas.Draw(Rect.Left,Rect.Top,Bitmap);
Bitmap.Free;
end;
procedure TForm2.ListView1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
ListItem: TListItem;
begin
ListItem := TListItem(ListView1.GetItemAt(X,Y));
if Assigned(ListItem) = True then
begin
if htOnStateIcon in ListView1.GetHitTestInfoAt(X, Y) then
begin
if ListItem.Enabled = True then
begin
ListItem.Checked := not ListItem.Checked;
end;
end;
end;
Exit;
end;
end.
ツイート | ![]() |