Windowsプログラミングメモ2
by K.I
Index
[top]
- ダイアログhdlg1上に置かれた、PaintコントロールIDC_GRAPHに、ダブルバッファhbufferの内容をコピーする。
- StretchBltを使って、拡大縮小してコピーする。ダブルバッファの大きさは、bufx×bufy。
RECT rt;
HWND hctrl;
HDC hdc;
hctrl = GetDlgItem(hdlg1,IDC_GRAPH);
GetWindowRect(hctrl,&rt);
hdc = GetDC(hctrl);
StretchBlt(hdc,0,0,rt.right-rt.left,rt.bottom-rt.top,
hbuffer,0,0,bufx,bufy,SRCCOPY);
ReleaseDC(hctrl,hdc);
- まずペンとブラシを設定する。現在のペンとブラシの状態は保存しておく。
save_pen = (HPEN) SelectObject(hbuffer,hpen);
save_brush = (HBRUSH) SelectObject(hbuffer,hbrush);
- 図形を描画する。以下は、Rectangle(矩形)描画の例
Rectangle(hbuffer,left,bottom,right,top);
SelectObject(hbuffer,save_brush);
SelectObject(hbuffer,save_pen);
- グラフで、値に応じた色を割当てる時、適当にやると汚い色になってしまう。
- HSV色空間で、Hだけを変化させれば比較的キレイなカラーテーブルを得ることが出来る。
// HSV色空間→RGBへ変換(HSVは0〜1に正規化)
COLORREF hsv2rgb(double h, double s, double v)
{
double i,f,p,q,t;
if ( s == 0 ){ // グレイスケール
return(RGB(v*255,v*255,v*255));
}
else { // S=0以外
if (1.0 <= h)
h -= 1.0;
h *= 6.0;
i = floor (h);
f = h - i;
p = v * (1 - s);
q = v * (1 - (s * f));
t = v * (1 - (s * (1 - f)));
if( i < 1 )
return(RGB(v*255,t*255,p*255));
else if( i < 2 )
return(RGB(q*255,v*255,p*255));
else if( i < 3 )
return(RGB(p*255,v*255,t*255));
else if( i < 4 )
return(RGB(p*255,q*255,v*255));
else if( i < 5 )
return(RGB(t*255,p*255,v*255));
else
return(RGB(v*255,p*255,q*255));
}
}
[top]
- サブクラス化っていうのは、最初はMSCのようなクラスライブラリを使う場合だと全く誤解していたんだけど、
- 既存のルーチンの処理を横取りして、追加したい処理を行ってから本来の処理に戻すようなことらしい。
- むしろSDKでいろいろ弄る場合には、既存のAPIにちょっとだけ機能追加するっていうことで、積極的に使いたい機能だ。
- ピクチャコントロールに、ビットマップを表示させていたが、再描画がうまく行かない。
- 下手に、ダイアログでWM_PAINTを処理したりすると、全体の描画が変に1なってしまう。
- いろいろ調べたところピクチャコントロールをサブクラス化して、
- そこでWM_PAINTイベントを捕まえて再描画させるのがスマートなやり方らしい。
- サブクラス化の準備としては、
- 本来のピクチャコントロールルーチンのアドレスを求める。
- ピクチャコントロールルーチンの代わりに、自分で作ったサブクラス化を行うルーチンのアドレスをセット。
- まず、以下のように本来のピクチャコントロールルーチンを、自分の作ったルーチンに入れ替える。
static BOOL init_piccon_subproc(HWND hwnd)
{
// ピクチャコントロールルーチンのアドレスを取得して保存しておく。
ppiccon = GetWindowLong(GetDlgItem(hwnd,IDC_GRAPH),GWL_WNDPROC);
// ピクチャコントロールルーチンのアドレスをサブクラス化ルーチンのアドレスに変更
SetWindowLong(GetDlgItem(hwnd,IDC_GRAPH),GWL_WNDPROC,(LONG)piccon_subproc);
return TRUE;
}
- サブクラス化を行うルーチンは、
- 最初に追加したい処理、この場合はWM_PAINTを処理する。
- あとは、本来のピクチャコントロールルーチンへ飛んで、通常の処理を行う。
LRESULT CALLBACK piccon_subproc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
PAINTSTRUCT ps;
HDC hdc;
RECT rt;
switch (msg) {
// 再描画処理を追加してみた
case WM_PAINT:
GetWindowRect(hWnd,&rt);
hdc = BeginPaint(hWnd, &ps);
StretchBlt(hdc,0,0,rt.right-rt.left,rt.bottom-rt.top,
cg.hbuffer,0,0,cg.bufx,cg.bufy,SRCCOPY);
EndPaint(hWnd, &ps);
break;
}
// 続けて、本来のピクチャコントロールの処理をおこなう
return CallWindowProc((WNDPROC)ppiccon,hWnd,msg,wp,lp);
}
- やってみると、ちょっとした機能拡張が簡単に出来て、且つプログラムがスッキリする。
- こんなに手軽で便利とは思わなかった。これからはどんどん使おう。
- クリッカブルURLとも呼ばれる、アンダーバーが付いたリンク文字列
- 文字にアンダーバーを付けるのは、 フォントの属性を変えれば良い
- リンク文字列を、Staticコントロールで作成して、サブクラス用のコントロールルーチンを用意する
- このコントロールルーチン内で、文字の色を青く書き直す。
- それから、文字列の上にカーソルを持ってきた時に、指差しカーソルに変えると、それらしくなる。
LRESULT CALLBACK linkProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg){
case WM_SETCURSOR: //マウスポインタの変更
SetCursor(g.hCur);
return 0;
case WM_PAINT: // リンク文字列を青く書き直す
PAINTSTRUCT ps;
HDC hdc;
char line[256];
GetWindowText(hWnd, line, 255);
hdc = BeginPaint(hWnd, &ps);
SelectObject(hdc, g.hLinkfont);
SetTextColor(hdc, RGB(0, 0, 255)); // 文字を青くする
SetBkColor(hdc, GetSysColor(COLOR_BTNFACE));
SetBkMode(hdc, OPAQUE);
TextOut(hdc, 0, 0, line, lstrlen(line));
EndPaint(hWnd, &ps);
return 0;
}
return CallWindowProc(oldlinkProc, hWnd, msg, wp, lp);
}
- DialogのWM_INITDIALOGで、リンク文字列のコントロールルーチンを、
- 用意したコントロールルーチンに置き換えてサブクラス化する。
//URLリンク文字列のサブクラス化
oldlinkProc = (WNDPROC)GetWindowLong(GetDlgItem(hdlg, IDC_LINKURL), GWL_WNDPROC);
SetWindowLong(GetDlgItem(hdlg, IDC_LINKURL), GWL_WNDPROC, (LONG)linkProc);
- あとは、Dialogルーチンに、実際にブラウザでURLを開くコードを追加する
- Clickを検出できるように、リンク文字列のリソースの NotifyをTrue に設定しておくこと。
case WM_COMMAND:
switch (LOWORD(wp)) {
case IDC_LINKURL:
if (HIWORD(wp) == STN_CLICKED) {
char line[256];
GetDlgItemText(hdlg, LOWORD(wp), line, 255);
ShellExecute(hdlg, "open", line, NULL, NULL, SW_SHOWDEFAULT);
}
break;
}
break;
- 猫でもでは、以下のようにコントロールルーチン内で、ほとんど完結するようにしている。
- やってることは同じなんだけど、この方がすっきりしている気がする。
LRESULT CALLBACK linkProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
HDC hdc;
PAINTSTRUCT ps;
COLORREF cr;
LOGFONT lf;
HFONT hFont;
HCURSOR hCursor;
switch (msg) {
case WM_SETCURSOR: // マウスポインタの変更
hCursor = LoadCursor(g.hInst, MAKEINTRESOURCE(IDI_CURSOR1));
SetCursor(hCursor);
return 0;
case WM_LBUTTONDOWN: // ブラウザでURLを開く
ShellExecute(hWnd, "open", g.linkurl, NULL, NULL, SW_SHOWDEFAULT);
return 0;
case WM_PAINT: // リンク文字列にアンダーバーを付けて青く書き直す
hdc = BeginPaint(hWnd, &ps);
memset(&lf, 0, sizeof(LOGFONT));
strcpy_s(lf.lfFaceName, "system");
lf.lfUnderline = TRUE;
hFont = CreateFontIndirect(&lf);
SelectObject(hdc, (HGDIOBJ)hFont);
cr = GetSysColor(COLOR_MENU);
SetBkColor(hdc, cr);
SetTextColor(hdc, RGB(0, 0, 255));
SetBkMode(hdc, TRANSPARENT);
TextOut(hdc, 0, 0, g.linkurl, strlen(g.linkurl));
DeleteObject(hFont);
EndPaint(hWnd, &ps);
return 0;
}
return (CallWindowProc(oldlinkProc, hWnd, msg, wp, lp));
}
- これならDialogにクリックが伝わらなくても良さそうだが、
- この場合でも、Link文字列のNotifyはTrueにする必要があるようだ。
1WM_PAINTをメインのWindow以外で行うのは、どうもマズイようだ。
[top]
- アプリケーション終了時にWindow位置を保存して、次回の起動時にその位置を復元するというのは、意外と難しい。
- ルーチェさんの ウィンドウ位置の正しい復元で説明されているものを、ほぼそのまま使用させて頂いています。
void SaveWindowState(HWND hWnd, char *symnam)
{
str255 line;
WINDOWPLACEMENT w;
// ウィンドウ位置・状態取得
w.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &w);
// ウィンドウ位置、状態保存(1〜4項目に位置、5項目にZoom状態)
if (w.showCmd == SW_SHOWMAXIMIZED) {
sprintf(line,"%d,%d,%d,%d,1",
w.rcNormalPosition.left,w.rcNormalPosition.bottom,
w.rcNormalPosition.right,w.rcNormalPosition.top);
}
else {
sprintf(line,"%d,%d,%d,%d,0",
w.rcNormalPosition.left,w.rcNormalPosition.bottom,
w.rcNormalPosition.right,w.rcNormalPosition.top);
}
set_symbol(symnam,line); // symnamで保存
}
void LoadWindowState(HWND hWnd, char *symnam)
{
WINDOWPLACEMENT w;
RECT rcWnd;
MONITORINFO mi;
str255 line;
// 既定のウィンドウ位置取得
w.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &w);
get_rectsym(&rcWnd,symnam,&w.rcNormalPosition); // 1〜4項目の前回のWin位置を取得
// 対象モニタの情報を取得
HMONITOR hMonitor = MonitorFromRect(&rcWnd, MONITOR_DEFAULTTONEAREST);
mi.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(hMonitor, &mi);
// 位置補正
if (rcWnd.right > mi.rcMonitor.right) {
rcWnd.left -= rcWnd.right - mi.rcMonitor.right;
rcWnd.right = mi.rcMonitor.right;
}
if (rcWnd.left < mi.rcMonitor.left) {
rcWnd.right += mi.rcMonitor.left - rcWnd.left;
rcWnd.left = mi.rcMonitor.left;
}
if (rcWnd.bottom > mi.rcMonitor.bottom) {
rcWnd.top -= rcWnd.bottom - mi.rcMonitor.bottom;
rcWnd.bottom = mi.rcMonitor.bottom;
}
if (rcWnd.top < mi.rcMonitor.top) {
rcWnd.bottom += mi.rcMonitor.top - rcWnd.top;
rcWnd.top = mi.rcMonitor.top;
}
// ウィンドウ位置復元
SetWindowPos(hWnd, NULL, rcWnd.left, rcWnd.top,
rcWnd.right - rcWnd.left, rcWnd.bottom - rcWnd.top, SWP_NOZORDER);
// ウィンドウ最大化状態復元 // 5項目の前回のZoom状態取得
get_dsym(line,symnam,",,,,0",255); // symnamから読出し
if (iitem(line,5)) ShowWindow(hWnd, SW_MAXIMIZE);
}
- Windowの最小・最大サイズの制限は、以下のように、WM_GETMINMAXINFOでサイズを指定すれば良い。
case WM_GETMINMAXINFO:
lpmm = (LPMINMAXINFO)lp;
lpmm->ptMinTrackSize.x = 100;
lpmm->ptMinTrackSize.y = 100;
lpmm->ptMaxTrackSize.x = 640;
lpmm->ptMaxTrackSize.y = 480;
break;
[top]
- データを表示する場合、TextBoxに表示しても良いが、大量のデータを扱うのは難しい。
- Excelのように、項目と行で管理するのが、ListView。
- ListViewは、基本的にはListBoxのように行のデータをItemとして管理する。
- 通常のリストビューでは、項目名の追加→行の追加→データの設定というようになる。
- 後述する仮想リストビューでは、項目名の追加と行数の指定のみで、データの管理は自分で行う必要がある。
- その場合、行の追加やデータのセットについては、以下のルーチンは使用しない。
- まず最初に項目名を作成する。ListView_InsertColumnというマクロを使用する。
void InsColumn(HWND hWnd, char *str, int cx, int iSub)
{
LV_COLUMN col;
col.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
col.fmt = LVCFMT_LEFT;
col.cx = cx;
col.pszText = str;
col.iSubItem = iSub;
ListView_InsertColumn(hWnd, iSub, &col);
return;
}
- hWnd:リストハンドル、str:項目名、cx:項目の幅、iSub:項目番号(0〜)
- 次に、行を追加する。これはListView_InsertItemマクロを使用。
- この例では一番左端のカラムにデータが追加される。データを空にすれば行の追加だけになる。
void InsItem(HWND hWnd, int item, char *str)
{
LV_ITEM itm;
itm.mask = LVIF_TEXT;
itm.pszText = str;
itm.iItem = item;
itm.iSubItem = 0;
ListView_InsertItem(hWnd, &itm);
return;
}
- 最後にデータをセットする。これもListViwe_SetItemマクロを使用する。
void SetSubitem(HWND hWnd, int item, int subitem, char *str)
{
LV_ITEM itm;
itm.mask = LVIF_TEXT;
itm.pszText = str;
itm.iItem = item;
itm.iSubItem = subitem;
ListView_SetItem(hWnd, &itm);
return;
}
- リストビューを作り直す時等は、以下のように全カラムを削除してから、新規にカラムを作る。
void listview_delallcol(HWND hlistview)
{
int i,count;
//カラム数を求める
count = Header_GetItemCount(ListView_GetHeader(hlistview));
for (i=0; i<count ;i++) {
ListView_DeleteColumn(hlistview,0);
}
}
- あと、ListView_DeleteAllItemsというマクロもあるみたいだけど、使ったことはない。
- 標準のListViewの特定の行や列に色を指定することは出来ないが、
- WindowのWM_NOTIFYメッセージでは、描画前のシーケンスCDDS_PREPAINTイベントを拾うことが出来る。
- ここで、CDRF_NOTYFYITEMDRAWでITEM描画情報をリクエストすると、
- ListViewのITEMを描画する直前に、CDDS_ITEMPREPAINTイベントも受けられるようになる。
- ITEMPREPAINTでTextの色を設定すれば、LisViewの行毎の色の変更が可能になる。(nmcd.dwItemSpecが行番号)
- ここでさらに、CDRF_NOTIFYSUBITEMDRAWでSUBITEM描画情報をリクエストすると、SUBITEM描画直前のイベントが受けられる。
- これは、CDDS_ITEMPREPAINT|CDDS_SUBITEMが結合したイベントとして拾えるので、
- この時、TEXTの色を設定すれば、ListViewの列毎の色の変更も可能になる。(iSubItemが列番号)
- 具体的には、以下のようにすれば良い。(以下はDialog中のコントロールでの例)
case WM_NOTIFY:
LPNMHDR lpnmhdr;
LPNMLVCUSTOMDRAW lplvcd;
lpnmhdr = (LPNMHDR)lp;
if (lpnmhdr->hwndFrom == g.hbtable) { // BINTABLEからのメッセージであることをチェック
switch ( lpnmhdr->code ) {
case NM_CUSTOMDRAW :
// ---- カスタムドローで、Listviewの色を設定。
lplvcd = (LPNMLVCUSTOMDRAW)lp;
switch (lplvcd->nmcd.dwDrawStage) {
case CDDS_PREPAINT: // 描画前にITEM情報をリクエスト
SetWindowLong(hWnd,DWL_MSGRESULT,(long)CDRF_NOTIFYITEMDRAW);
return TRUE;
case CDDS_ITEMPREPAINT: // ITEM描画前にSUBITEM情報をリクエスト
SetWindowLong(hWnd,DWL_MSGRESULT,(long)CDRF_NOTIFYSUBITEMDRAW);
return TRUE;
case CDDS_ITEMPREPAINT|CDDS_SUBITEM: // SUBITEM描画イベント
row = lplvcd->nmcd.dwItemSpec; // 行番号
col = lplvcd->iSubItem; // 列番号
ListView_GetItemText(g.hbtable,row,col,line,255);
i = atoi(line)*10%256;
lplvcd->clrTextBk = RGB(255,255-i,255-i); //値により色を設定
lplvcd->clrText = RGB(0, 0, 0);
SetWindowLong(hWnd,DWL_MSGRESULT,(long)CDRF_NEWFONT);
return TRUE;
}
break;
default :
break;
} // end of switch(lpnmhdr->code)
} // end of (hwndFrom == g.hbtable)
return TRUE;
カスタムドローは最初はうまく行かなかった。自分が引っ掛かったのは以下のようなこと。
- Windowと、DialogではCDRF_NOTIFYITEMDRAWやCDRF_NEWFONTの渡し方2が違う。
- Windowでは、単にreturnの引数で返せば良いが、DialogではSetWindowLongで渡して、return TRUEする必要がある。
- SUBITEMの描画情報の条件は、lplvcd->nmcd.dwDrawStage==CDDS_ITEMPREPAINT|CDDS_SUBITEMであって、
- lplvcd->nmcd.dwDrawStage==CDDS_ITEMPREPAINT || lplvcd->nmcd.dwDrawStage==CDDS_SUBITEMではない。
- ListViewは便利だが、あまり大量のデータを扱うように出来ていない。
- 項目数が多いと、動作が極端に遅くなってしまう。
- 元々、データテーブルを持っている場合には、リストビューにコピーする手間も掛かる。
- データの保持はプログラマに任せて、項目数の管理と表示のみ行うのが、仮想リストビュー。
- 表示する項目のデータを、都度要求して表示するだけなので、高速。
手順としては、
- まずListViewのリソースプロパティで、OwnerDataをTrueにしておく。
- 項目名を追加する。これは通常のListViewと同じ。
- 次に、以下のようにデータの行数(アイテム数)を指定する。
ListView_SetItemCountEx( g.hbtable, 9999, LVSICF_NOINVALIDATEALL );
あとは、WindowのWM_NOTIFYメッセージで、LVN_GETDISPINFOイベントを拾って、
- 指定された行,列のデータをpszTextに返せば良い。(iSubItem:列番号、iItem:行番号)
case WM_NOTIFY:
LPNMHDR lpnmhdr;
lpnmhdr = (LPNMHDR)lp;
if (lpnmhdr->hwndFrom == g.hbtable) { // BINTABLEからのメッセージであることをチェック
switch ( lpnmhdr->code ) {
case LVN_GETDISPINFO :
LV_DISPINFO *pLvDispInfo;
pLvDispInfo = (LV_DISPINFO*)lp;
TCHAR szString[MAX_PATH];
if (pLvDispInfo->item.mask & LVIF_TEXT) { // TEXTならば
col = pLvDispInfo->item.iSubItem; // 列番号
row = pLvDispInfo->item.iItem; // 行番号
wsprintf(szString,"%d,%d",col,row); // とりあえず列行を出力
if (lstrlen(szString) < pLvDispInfo->item.cchTextMax)
lstrcpy(pLvDispInfo->item.pszText,szString);
else
lstrcpy(pLvDispInfo->item.pszText,_T(""));
}
break;
default :
break;
} // end of switch(lpnmhdr->code)
} // end of (hwndFrom == g.hbtable)
return TRUE;
- TEXTならば、item.maskがLVIF_TEXTの場合だけ処理する。
- データは自分で管理するので、ListViewのデータの設定や保存も自前で行う必要がある。
- そのため、ListViwe_SetItemマクロによる、行の追加やデータの設定は行わない。
- 実際に表示するデータしか読書きしないので、巨大なデータでも高速。
- データをファイルから読んだり、自動設定しても良いので自由度が高く、小さなデータでもメリットが大きいと思う。
- 前述のカスタムドローと組み合わせて使用することも出来る。
- 背景色や、フォントの変更ぐらいならば、カスタムドローが簡単。
- 全て自分で描画してしまうのを、オーナードローと言う。
- 手順としては、まずListViewのリソースプロパティで、OwnerDrawFixedをTrueにしておく
- これでデータ表示はプログラマに任されることになる
- 具体的には、これでWM_DRAWITEMイベントが発生するようになるので、これを捕まえて表示すれば良い。
:
case WM_DRAWITEM: // オーナードロー
DrawListItem((LPDRAWITEMSTRUCT)lp);
return TRUE;
:
void DrawListItem(LPDRAWITEMSTRUCT lpDraw)
{
HWND hList; //リストビューのハンドル
HDC hdc; //デバイスコンテキスト
RECT rt;
HBRUSH hBrush; //背景描画用のブラシ
LVCOLUMN LvColumn; //列項目取得用の構造体
int SubItemNum;
int SubItem;
str255 Text;
hList=lpDraw->hwndItem;
hdc = lpDraw->hDC;
SaveDC(hdc); // デバイスコンテキスト保存
SetTextColor(hdc, RGB(0,0,255)); // 文字色を設定
//カラムヘッダ情報取得用
ZeroMemory(&LvColumn,sizeof(LvColumn));
LvColumn.mask=LVCF_FMT;
//列の数を取得する
SubItemNum=Header_GetItemCount(ListView_GetHeader(hList))-1;
//SubItemの表示
for (SubItem=0; SubItem<=SubItemNum ;SubItem++) {
//SubItemの大きさを取得
ListView_GetSubItemRect(hList,lpDraw->itemID,SubItem,LVIR_BOUNDS,&rt);
//カラムヘッダの情報取得
ListView_GetColumn(hList,SubItem,&LvColumn);
ListView_GetSubItemRect(hList,lpDraw->itemID,SubItem,LVIR_LABEL,&rt);
//アイテムの文字情報を取得
ListView_GetItemText(hList,lpDraw->itemID,SubItem,Text,sizeof(Text));
//背景色の塗潰し
hBrush = CreateSolidBrush(RGB((atoi(Text)%256)*10,0,0));
FillRect(hdc,&rt,hBrush);
DeleteObject( hBrush ); // 作成したブラシを廃棄
// SubItemを表示
DrawListItemText(hdc,Text,&rt,LvColumn.fmt);
}
//デバイスコンテキストを復帰
RestoreDC(hdc,-1);
}
- 上記の例は単なる文字列表示のみで、選択時の処理等は省略している。
LV_HITTESTINFO lvinfo;
LV_ITEM item;
str255 buf;
:
// WN_NOTIFYのLVN_ITEMCHANGED等の処理
GetCursorPos((LPPOINT)&lvinfo.pt);
ScreenToClient(((LPNMLISTVIEW)lp)->hdr.hwndFrom, &lvinfo.pt);
ListView_HitTest(((LPNMLISTVIEW)lp)->hdr.hwndFrom, &lvinfo);
if ((lvinfo.flags & LVHT_ONITEM) != 0) {
item.mask = TVIF_HANDLE | TVIF_TEXT;
item.iItem = lvinfo.iItem;
item.iSubItem = 0; // サブアイテム番号
item.pszText = buf; // テキストバッファ
item.cchTextMax = 255; // バッファ容量
ListView_GetItem(((LPNMLISTVIEW)lp)->hdr.hwndFrom, &item);
MessageBox(hWnd, buf,"Click", MB_OK);
}
この例では、カーソルキーで移動した場合も最初のクリック位置しか分からない。
- でも、ListView上にチェックボックス等を置きたい場合等に使える。
ListView_SetItemState(g.hList, n, LVIS_SELECTED, LVIS_SELECTED)
ListView_SetItemState(g.hList, n, 0, LVIS_SELECTED)
ListView_SetItemState(g.hList, -1, LVIS_SELECTED, LVIS_SELECTED)
ListView_SetItemState(g.hList, -1, 0, LVIS_SELECTED)
- ListViewは、初期状態では複数選択可能なので、
- ListViewのAlways Show SelectionをTrueにしない場合に限り、
- フォーカスが外れている場合の選択色は、カスタムドローで設定可能。
case WM_NOTIFY:
:
case NM_CUSTOMDRAW:
lplvcd = (LPNMLVCUSTOMDRAW)lp;
switch (lplvcd->nmcd.dwDrawStage) {
case CDDS_PREPAINT: // 描画前にITEM情報をリクエスト
SetWindowLong(hWnd, DWL_MSGRESULT, (long)CDRF_NOTIFYITEMDRAW);
break;
case CDDS_ITEMPREPAINT:
row = lplvcd->nmcd.dwItemSpec; // 行番号
if (ListView_GetItemState(g.hList, row, LVIS_SELECTED)) {
lplvcd->clrTextBk = RGB(0, 255, 255);
lplvcd->clrText = RGB(0, 0, 0);
SetWindowLong(hWnd, DWL_MSGRESULT, (long)CDRF_NEWFONT);
}
break;
}
break;
- 但し、ListViewにフォーカスされている場合の選択色を変える方法は分からなかった。
- こちらの方法でフォーカス時の選択色を無くすことが出来たが3結局、色設定は出来なかった。
- とにかく、Always Show SelectionをONにして、フォーカスが外れたときの選択色が薄い灰色で物凄く見づらかったので、
- それだけでも設定できれば良いやということで、これ以上は追求しない。
- ListViewを描画無効領域に設定して、再描画させる。
InvalidateRect(g.hList, NULL, TRUE);
- ListViewの簡単な使い方の例。(VC5でも動作可能)
- 仮想ListViewを使うので、Owner Dataを True に
- カラムを持っているので、Viewを Report にしておく
- 項目名の設定には、前述のInsColumnルーチンを使用。
DWORD dwStyleEx=0;
NM_LISTVIEW *pNMLV;
case WM_INITDIALOG: // Dialogの初期設定
// リストハンドルの取得
g.hList = GetDlgItem(hWnd,IDC_REGLIST);
// 1行選択と罫線の表示
dwStyleEx = ListView_GetExtendedListViewStyle( g.hList );
dwStyleEx |= ( LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT | LVS_EX_ONECLICKACTIVATE);
// dwStyleEx |= ( LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT );
ListView_SetExtendedListViewStyle( g.hList, dwStyleEx );
// 項目名の設定
InsColumn(g.hList,"名前",50,0);
InsColumn(g.hList,"内容",200,1);
InsColumn(g.hList,"設定",50,2);
// 行数の設定(データ数に合わせて設定しておく)
ListView_SetItemCountEx(g.hList, 9999, LVSICF_NOINVALIDATEALL );
// とりあえず、1行目を選択状態にしておく
ListView_SetItemState(g.hList,0,LVIS_FOCUSED|LVIS_SELECTED,LVIS_FOCUSED|LVIS_SELECTED);
case WM_NOTIFY:
LPNMHDR lpnmhdr;
lpnmhdr = (LPNMHDR)lp;
switch(((LPNMHDR)lp)->idFrom) {
case IDC_REGLIST:
switch ( lpnmhdr->code ) {
case LVN_GETDISPINFO : // 仮想ListViewの表示データ問合せ
LV_DISPINFO *pLvDispInfo;
pLvDispInfo = (LV_DISPINFO*)lp;
TCHAR szString[MAX_PATH];
if (pLvDispInfo->item.mask & LVIF_TEXT) {
col = pLvDispInfo->item.iSubItem; // 列番号
row = pLvDispInfo->item.iItem; // 行番号
switch(col) {
case 0:
wsprintf(szString,"%s",d[row].name); // 名前をセット
break;
case 1:
wsprintf(szString,"%s",d[row].desc); // 内容をセット
break;
case 2:
wsprintf(szString,"%d",d[row].data); // 設定値をセット
break;
}
if (lstrlen(szString) < pLvDispInfo->item.cchTextMax)
lstrcpy(pLvDispInfo->item.pszText,szString); // 表示文字列を返す
else
lstrcpy(pLvDispInfo->item.pszText,_T("****")); // 文字列が大きすぎる場合
}
break;
// case LVN_ITEMCHANGED:
case LVN_ITEMACTIVATE:
row = ListView_GetNextItem(g.hList,-1,LVIS_SELECTED); // 選択行を求める
if (row!=-1)
printf(">%d\n",row);
break;
case LVN_COLUMNCLICK: // カラムヘッダ部のクリック
pNMLV = (NM_LISTVIEW *)lp;
printf(">col %d click\n",pNMLV->iSubItem);
break;
default :
break;
}
}
return TRUE;
- WM_NOTIFYイベントで、表示する行と列の内容を問い合わせてくるので、
- LVN_ITEMACTIVATEは、デフォルトでは2クリックでアクティブになるが、
- LVS_EX_ONECLICKACTIVATEをセットすることで、1クリックでアクティブになる。
- 試しに、これでクリックを検出するようにしてみたが、少し反応が鈍い気がする。
- 普通に、LVN_ITEMCHANGEDや、NM_DBLCLICKイベントを使う方が良いかもしれない。
- 基本的にListViewは行単位の選択しかサポートしていない4ので、
- ListViewの選択状態をプログラムで変更するには、ListView_SetItemStateを使う。
- 3番目のパラメータが設定するビットの指定で、4番目がマスク。
- 例えば非選択状態にするためには、3番目に0を指定して、4番目にLVIS_SELECTEDを指定する。
- VC5では、リソースViewでオーナーデータが設定出来ない(オーナーデータのチェックボックスが出ない)ので、仮想ListViewが使えないと思っていたけど、
- CreateWindowのスタイルで設定すれば、問題なく仮想リストビューが使える。
- リソースViewでリソースを作成した後に、テキストエディタで、rcファイルを編集してLVS_OWNERDATAを追加しておけば、無問題。
CONTROL "List1",IDC_REGLIST,"SysListView32",LVS_REPORT |
LVS_OWNERDATA | WS_BORDER | WS_TABSTOP,131,23,216,154
2CDDS_ITEMPREPAINTイベントが来ない場合はコレを疑うこと。
3フォーカスが何処だか分からなくなってしまった。
4ListViewの列方向は取って付けたような中途半端な仕様。初めから2次元を扱うように作ってあれば、すっきりしたのに。
[top]
- キー入力は、WM_KEYDOWN,WM_KEYUPイベントで、wpにキーコードが入るので、
case WM_KEYDOWN:
switch (wp) {
case VK_RIGHT:
// →キーの処理;
break;
case VK_LEFT:
// ←キーの処理;
break;
}
return TRUE;
- 普通のキー、例えばAキー等は、アスキーコード0x41になっている。
- 例えば、TABキーとCTRLキーの同時押し等は、GetAsyncKeyStateを使って判定する。
skey = (GetAsyncKeyState(VK_SHIFT) && 0x8000);
ckey = (GetAsyncKeyState(VK_CONTROL) && 0x8000);
if (wp==VK_TAB) {
if (ckey) ;// CTRLキーとTABキーの同時押し処理
if (skey) ;// SHIFTキーとTABキーの同時押し処理
}
自分の環境では、CTRLキーとCapsLockキーを入替えてある所為か、CTRLキーの認識がうまく行かない。
- 基本的に検出出来ないんだけど、何回かやると検出できるから、ちょっと始末が悪い。
- CapsLockキーって何かあるのかな。なんかいい方法は無いものかなぁ。
- でもDialogの場合、コントロールがキー入力を処理してしまうため、キー入力イベントが発生しない。
- そのため、Dialogのキー入力処理をフックして、キー入力イベントが発生するようにしてやる必要がある。
- まず、WM_INITDIALOG等で、 キー入力処理用フックルーチンを呼ぶようにしておく。
- hHookは、HHOOK型のハンドル。
g.hHook = SetWindowsHookEx(WH_KEYBOARD,(HOOKPROC)KeyboardProc,NULL, GetWindowThreadProcessId(hWnd,NULL));
- そして、以下のようなキー入力処理用フックルーチンを記述する。
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wp, LPARAM lp)
{
//nCodeが0未満のときは、CallNextHookExが返した値を返す
if (nCode < 0) return CallNextHookEx(g.hHook,nCode,wp,lp);
if (nCode==HC_ACTION) {
//キーの遷移状態のビットをチェックして
//WM_KEYDOWNとWM_KEYUPをDialogに送信する
if ((lp & 0x80000000)==0) {
SendMessage(g.hDlg1,WM_KEYDOWN,wp,lp);
}
else {
SendMessage(g.hDlg1,WM_KEYUP,wp,lp);
}
}
return CallNextHookEx(g.hHook,nCode,wp,lp);
}
結局のところ、自分の場合はGUIのインターフェースでは、Dialogを使うのは普通なので、
- 通常、このようなキー入力用フックルーチンを記述しなければならないってことか。
[top]
- 以下のように記述すると、DLLを実際に使用するまで、Loadされない。(VCの場合のみ)
#pragma comment(lib, "unknown.lib")
#pragma comment(linker, "/delayload:unknown.dll")
- ワケの分からないDLLから、LIBファイルを作る。VC5に付属のdumpbin.exe、lib.exeを使用する。(link.exeも要るかも)
- すると、DLLの概要が表示されるので
:
ordinal hint name
1 0 UnknownFunc1 (00005424)
2 1 UnknownFunc2 (00005441)
Summary
:
- これを基に、エディタでunknown.defファイルを作成する。
LIBRARY unknown.dll
EXPORT
UnknownFunc1 →dumpbinで表示されたDLL関数名を列挙する
UnknownFunc2
- defファイルから、libコマンドでLIBファイルを生成する。
>lib /def:unknown.def /machine:x86
- DLLからLIBを作る方法で関数名を調べると、以下の4つの関数が出てくる場合がある。
- これは、ActiveX DLLというものらしい。これは普通のDLLと違って、ヘッダ情報を持っている。
- VC5の場合は、表示→ClassWizard→オートメーションタブで、
- クラスの追加→タイプライブラリからを選んで、DLL自体を選択すると、
- VC++用のラップクラスと、ヘッダファイルが自動生成される。
- なんて、便利なんだろう!
- VisualStudioではやったことないけど、多分簡単に出来ると思う。
[top]
- ファイルOpenダイアログを表示して、ファイル名を取得する。
#include <commdlg.h>
:
BOOL openfile_dialog(HWND hWnd, TCHAR *defa_file, TCHAR *title, TCHAR *filter)
{
OPENFILENAME OpenFileName;
OpenFileName.lStructSize = sizeof(OPENFILENAME);
OpenFileName.hwndOwner = hWnd;
OpenFileName.hInstance = NULL;
OpenFileName.lpstrFilter = filter;
OpenFileName.lpstrCustomFilter = NULL;
OpenFileName.nMaxCustFilter = 0;
OpenFileName.nFilterIndex = 0;
OpenFileName.lpstrFile = defa_file;
OpenFileName.nMaxFile = 511;
OpenFileName.lpstrFileTitle = NULL;
OpenFileName.nMaxFileTitle = 0;
OpenFileName.lpstrInitialDir = NULL;
OpenFileName.lpstrTitle = title;
OpenFileName.nFileOffset = 0;
OpenFileName.nFileExtension = 0;
OpenFileName.lpstrDefExt = NULL;
OpenFileName.lCustData = NULL;
OpenFileName.Flags = OFN_EXPLORER | OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT;
return GetOpenFileName(&OpenFileName);
}
if (openfile_dialog(NULL,file_name,"ファイルOpen",
"設定ファイル(*.dat)\0*.dat\0AllFile(*.*)\0*.*\0\0")) {
// 読込み処理
}
- GetOpenFileNameの代わりに、GetSaveFileNameを使う。
- 構造体は同じOpenFileNameを使うので、まぁほとんど同様にすれば良い。
BOOL savefile_dialog(HWND hWnd, TCHAR *defa_file, TCHAR *title, TCHAR *defext, TCHAR *filter)
{
OPENFILENAME OpenFileName;
OpenFileName.lStructSize = sizeof(OPENFILENAME);
OpenFileName.hwndOwner = hWnd;
OpenFileName.hInstance = NULL;
OpenFileName.lpstrFilter = filter;
OpenFileName.lpstrCustomFilter = NULL;
OpenFileName.nMaxCustFilter = 0;
OpenFileName.nFilterIndex = 0;
OpenFileName.lpstrFile = defa_file;
OpenFileName.nMaxFile = 511;
OpenFileName.lpstrFileTitle = NULL;
OpenFileName.nMaxFileTitle = 0;
OpenFileName.lpstrInitialDir = NULL;
OpenFileName.lpstrTitle = title;
OpenFileName.nFileOffset = 0;
OpenFileName.nFileExtension = 0;
OpenFileName.lpstrDefExt = defext; // 拡張子が指定されない場合のデフォルト拡張子
OpenFileName.lCustData = NULL;
OpenFileName.Flags = OFN_OVERWRITEPROMPT; // 上書きの確認をする
return GetSaveFileName(&OpenFileName);
}
- 使い方
if (savefile_dialog(NULL,file_name,"ファイル保存","dat",
"設定ファイル(*.dat)\0*.dat\0AllFile(*.*)\0*.*\0\0")) {
// 書込み処理
}
- Flagsで、以下の4つのフラグを追加
- OFN_EXPLORER // Explorer Style
- OFN_ENABLESIZING // サイズ変更可とする
- OFN_ENABLEHOOK // フック関数を使う
- OFN_ENABLETEMPLATE // ダイアログテンプレートを使う
- あと、フック関数アドレスと追加するDialogリソースを指定
- OpenFileName.lpfnHook = FookFileDialog; // フック関数アドレス
- OpenFileName.lpTemplateName = MAKEINTRESOURCE(IDD_CUSTOMFILEDIALOG);
UINT CALLBACK FookFileDialog(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch(msg) {
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
switch(LOWORD(wp)){
case IDC_BUTTON1:
// ボタンの処理
return TRUE;
}
break;
}
return FALSE;
}
- Dialogリソースの作成
- Dialogを新規追加して、スタイルをチャイルド、境界線なし、兄弟ウィンドウをクリップに設定する。
- リソースIDが0x45FのStaticコントロールを置いて、
- その下に、ボタンを置く。(追加するコントロールを適当に配置)
- 0x45FのStaticコントロールの下に追加したので、標準Dialogの下にコントロールが追加されたDialogとなる。
- 0x45FのStaticコントロールの上に追加すると、コントロールは上に追加される。
- こちらのコードを使わせて頂く
- 予めDialog上に、IDC_DESFILEというIDの、EditTextコントロールを置いておく
#include <shlobj.h>
int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
if (uMsg == BFFM_INITIALIZED){
SendMessage(hwnd, BFFM_SETSELECTION, (WPARAM)TRUE, lpData); //初期フォルダ設定
}
return 0;
}
void GetFolder(HWND hdlg)
{
char dst_file[MAX_PATH];
BROWSEINFO binfo;
LPITEMIDLIST idlist;
GetDlgItemText(hdlg, IDC_DESFILE, dst_file, MAX_PATH); //初期フォルダ読込み
binfo.hwndOwner = hdlg;
binfo.pidlRoot = NULL;
binfo.pszDisplayName = dst_file;
binfo.lpszTitle = "フォルダを指定してください";
binfo.ulFlags = BIF_RETURNONLYFSDIRS;
binfo.lpfn = &BrowseCallbackProc; //コールバック関数
binfo.lParam = (LPARAM)dst_file; //コールバックに渡す引数
binfo.iImage = (int)NULL;
idlist = SHBrowseForFolder(&binfo);
SHGetPathFromIDList(idlist, dst_file); //ITEMIDLISTからパスを得る
CoTaskMemFree(idlist); //ITEMIDLISTの解放
SetDlgItemText(hdlg, IDC_DESFILE, dst_file); //フォルダ名出力
}
- 初期フォルダを設定するために、コールバック関数を使用している。
- 初期フォルダを、Dialog上のEditTextコントロールに入力して、GetFolderを呼び出す
- 入力したフォルダ名を、Dialog上のコントロールに戻すようにしてみた。
- DlgDirListで、フォルダのファイル名をListBoxに表示することが出来る。
- ダブルクリックで、ListBoxに登録しなおす様にしている
char path[MAX_PATH] = { "C:\\*.*" };
:
case WM_INITDIALOG:
DlgDirList(hWnd, path, IDC_LISTBOX1, IDC_EDIT1, DDL_DIRECTORY | DDL_DRIVES); // ドライブ名も表示する
break;
case WM_COMMAND:
switch(LOWORD(wp)) {
case IDC_LIST1:
if (HIWORD(WP) == LBN_DBLCLK) {
if (DlgDirSelectEx(hWnd,path,sizeof(path),IDC_LISTBOX1)) {
strcat_s(path,"*.*");
DlgDirList(hWnd,path,IDC_LISTBOX1,IDC_EDIT1,DDL_DIRECTORY | DDL_DRIVES);
}
}
break;
- 簡単に、ファイル一覧を出すことが出来て便利だが、
- ファイルリストしか表示できないので、いまいち使い勝手が良くない。
- ファイルサイズと日付を含めて、ファイル一覧を求める。
int filelist(char *path)
{
WIN32_FIND_DATA fd;
HANDLE hFind;
FILETIME ft;
SYSTEMTIME st;
int i=0;
hFind = FindFirstFile(path, &fd);
while (hFind != INVALID_HANDLE_VALUE) {
if (fd.cFileName[0] != '.') {
strcpy_s(d[i].fname, fd.cFileName); // ファイル名
if (!(fd.dwFileAttributes&(FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_SYSTEM)))
d[i].fsize = (fd.nFileSizeHigh << 6) + ((fd.nFileSizeLow + 1023) >> 10); // ファイルサイズ[KB]
//d[i].fsize = fd.nFileSizeHigh*MAXDWORD + fd.nFileSizeLow; // ファイルサイズ
FileTimeToLocalFileTime(&(fd.ftLastWriteTime), &ft);
FileTimeToSystemTime(&ft, &st);
wsprintf(d[i].fdate, "%04d/%02d/%02d %02d:%02d",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute); // ファイル日付
i++;
}
if (!FindNextFile(hFind, &fd)) break;
}
FindClose(hFind);
return i;
}
- というより、プログラム中から指定したファイルを関連付けられたアプリケーションで実行する。
- アプリケーションを指定することも出来る。
#include <shellapi.h>
:
ShellExecute(NULL,NULL,"notepad",file_name,NULL,SW_SHOWNORMAL);
ShellExecute(NULL,NULL,"https://bluefish.orz.hm/",NULL,NULL,SW_SHOWNORMAL);
- エディットボックスに上下の矢印ボタンを組み合わせて、数値を入力するためのスピンコントロールの使い方。
- EditBoxと、その直後のTABオーダのスピンコントロールが連動する。
- プログラムで、EditBoxの値を書き直す必要が無いので便利。
- EditBoxのプロパティで、Number→TRUEに設定。
- Spinコントロールのプロパティで、AutoBuddy→TRUE、SetBuddyInteger→TRUEに設定。
- Alignment→RightAlignすれば、EditBoxの右側にSpinコントロールが付くはずだが、コントロールの設定がうまく行かなかった。
- あとはスピンコントロールに、以下のように設定すれば良い。
- EDITBOXの直後のTABオーダーにスピンコントロールを設定すれば、UDM_SETBUDDYの設定は不要かも。
SendMessage(GetDlgItem(hWnd, IDC_INTERVAL_C), UDM_SETBUDDY, (WPARAM)GetDlgItem(hWnd, IDC_INTERVAL), 0); // 対応するEditBox指定
SendMessage(GetDlgItem(hWnd, IDC_INTERVAL_C), UDM_SETRANGE, (WPARAM)0, (LPARAM)MAKELONG(8,1)); // 範囲指定
SendMessage(GetDlgItem(hWnd, IDC_INTERVAL_C), UDM_SETPOS, 0, (LPARAM)MAKELONG((short)g.interval, 0)); // 初期値の指定
- これで、スピンコントロールの操作によって、自動的にEditBoxの内容が変更される。
[top]
このメモを記述するにあたって、参考にさせて頂いたサイト。或いは参考になりそうなサイト。
[top]
[プログラムの部屋に戻る]
⇒ Disqusの広告がうるさすぎるので基本は非表示にしました