ウィンドウズプログラミング講座第9回:バイナリファイルビュワー
概要
ドキュメントの読み込みと保存に対応するため、何か作らなきゃと思いバイナリファイルビュワーにしました。
ま、バイナリファイルビュワーはそれほどでもないんだけど、ウィンドウサイズ変更に伴う処理は大変でした。
ファイル分割
何かと今後のことを考えてソースファイルを分割しました。あとクラスもちょこっと。
- Window.h & Window.cpp
- ウィンドウクラス
- View.h & View.cpp
- ビューウィンドウクラス
- main.h & main.cpp
- メイン
- App.h & App.cpp
- アプリケーションクラス
- DocView.h & DocView.cpp
- ドキュメントビュークラス
解説
今後ツリービューとプロパティビュー、そして出力ビューを増やす予定なのでビューウィンドウクラスを作りました。
今のところスクロールに対応するだけのクラスです。
なのでドキュメントビュークラスは、スクロールのことはビューウィンドウクラスに任せてドキュメントの処理に専念できます。
あとグローバル変数と関数はメインに残して、アプリケーションクラスは別ファイルにしました。
ウィンドウクラス
lparam を共用体にしました。
class Window { HWND hwnd; UINT umsg; WPARAM wparam; union { LPARAM lparam; HWND hctrl; LPNMHDR lpnmhdr; LPNMTTDISPINFO lpnmtdi; LPMINMAXINFO lpmmi; }; };
解説
前は一々個別のメンバ変数に代入してたんだけど、共用体にすりゃ良いじゃんって突然気が付きました。我ながらアホですね。
ビューウィンドウクラス
スクロール処理を行う子ウィンドウクラスです。スクロールの処理は前とほぼ同じなので説明は省略です。
変更点としてはスクロール単位を文字サイズ単位にしたことぐらいでしょうか。
グローバル(メイン)
ここからがメインということで上の2つは完全に独立です。ここから下は関連してるので切り離せません。
今のところグローバルなのは以下の通りです。
- extern INT GetProfInt( LPCTSTR section, LPCTSTR key, INT value );
- 整数型プロファイルデータ取得関数
- extern VOID SetProfInt( LPCTSTR section, LPCTSTR key, INT value );
- 整数型プロファイルデータ設定関数
- extern DWORD GetProfString( LPCTSTR section, LPCTSTR key, LPTSTR text, DWORD size );
- 文字列型プロファイルデータ取得関数
- extern VOID SetProfString( LPCTSTR section, LPCTSTR key, LPCTSTR text );
- 文字列型プロファイルデータ設定関数
- extern const LPCTSTR appName;
- アプリケーション名
- extern const LPCTSTR verName;
- バージョン名
- extern const LPCTSTR mutexName;
- extern const LPCTSTR iniFileName;
- イニファイル名
特に大きな変更はないですが、ミューテックス判定を WinMain() 関数内で行うようにしました。
アプリケーションクラス
ファイル関係の処理をドキュメントビューに関連付けました。
サイズ変更に伴う処理が大変でした。特に処理順序が。
初期化
BOOL App :: Init( int nCmdShow ) { dwStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; hWnd = CreateWindow( className, NULL, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, GetModuleHandle( NULL ), this ); InitMenu(); InitAccel(); InitToolBar(); InitStatBar(); InitDocView(); left = GetProfInt( sectionName, keyLeft, 0 ); top = GetProfInt( sectionName, keyTop, 0 ); width = GetProfInt( sectionName, keyWidth, 0 ); height = GetProfInt( sectionName, keyHeight, 0 ); bToolBar = GetProfInt( sectionName, keyToolBar, TRUE ); bStatBar = GetProfInt( sectionName, keyStatBar, TRUE ); if ( GetProfInt( sectionName, keyMaximize, FALSE ) ) { nCmdShow |= SW_MAXIMIZE; } if ( ( left >= 0 && left < GetSystemMetrics( SM_CXSCREEN ) ) && ( top >= 0 && top < GetSystemMetrics( SM_CYSCREEN ) ) && ( width > 0 && width <= GetSystemMetrics( SM_CXSCREEN ) ) && ( height > 0 && height <= GetSystemMetrics( SM_CYSCREEN ) ) ) { MoveWindow( hWnd, left, top, width, height, FALSE ); } if ( bToolBar ) { ShowWindow( hToolBar, SW_SHOW ); } else { ShowWindow( hToolBar, SW_HIDE ); } if ( bStatBar ) { ShowWindow( hStatBar, SW_SHOW ); } else { ShowWindow( hStatBar, SW_HIDE ); } AdjustSize(); UpdateMenu(); ShowWindow( hWnd, nCmdShow ); UpdateWindow( hWnd ); return TRUE; }
解説
ウィンドウって表示する前でもサイズが有効なんですね。
AdjustSize() 関数でウィンドウサイズを調整してるんですが、この時点ではまだ表示されてません。
サイズ調整
ツールバーとステータスバーの表示状態に応じてドキュメントビューの配置を調整します。
あとメインウィンドウの最小サイズのチェックもします。
VOID App :: AdjustSize() { RECT rcClient; RECT rc; if ( ! IsIconic( hwnd ) ) { UpdateMinSize(); if ( width < minWidth ) { width = minWidth; } if ( height < minHeight ) { height = minHeight; } if ( ! IsZoomed( hwnd ) ) { SetWindowPos( hwnd, 0, 0, 0, width, height, SWP_FRAMECHANGED | SWP_NOMOVE ); } GetClientRect( hwnd, & rcClient ); if ( bToolBar ) { GetWindowRect( hToolBar, & rc ); rcClient.top += ( rc.bottom - rc.top ); } if ( bStatBar ) { GetWindowRect( hStatBar, & rc ); rcClient.bottom -= ( rc.bottom - rc.top ); } MoveWindow( docView.hwnd, rcClient.left, rcClient.top, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, TRUE ); } }
解説
UpdateMinSize() 関数で現時点のウィンドウ最小サイズを計算します。
ツールバーとかの状態によって最小サイズは変わるんです。何故かってドキュメントウィンドウの最小サイズが決まっているからです。
やっかいなのは、ウィンドウの幅を縮めるとメニューバーが2行になったりするじゃないですか。これもちゃんと考慮しないといけません。
んで、現在のウィンドウサイズが最小値より小さければ最小値にしますが、最大化されてるときに SetWindowPos() 関数をやると大変なことになるのでチェックしてます。
ウィンドウ最小値の計算
メインウィンドウの最小サイズを現在の状態に合わせて更新します。
VOID App :: UpdateMinSize() { DWORD dwStyle = GetWindowLong( docView.hwnd, GWL_STYLE ); DWORD dwExStyle = GetWindowLong( docView.hwnd, GWL_EXSTYLE ); RECT rcDocView = { 0, 0, docView.minWidth, docView.minHeight }; RECT rcWindow; RECT rcClient; RECT rc; AdjustWindowRectEx( & rcDocView, dwStyle, FALSE, dwExStyle ); GetWindowRect( hwnd, & rcWindow ); GetClientRect( hwnd, & rcClient ); minWidth = ( rcWindow.right - rcWindow.left ) - ( rcClient.right - rcClient.left ) + ( rcDocView.right - rcDocView.left ); minHeight = ( rcWindow.bottom - rcWindow.top ) - ( rcClient.bottom - rcClient.top ) + ( rcDocView.bottom - rcDocView.top ); if ( bToolBar ) { GetWindowRect( hToolBar, & rc ); minHeight += rc.bottom - rc.top; } if ( bStatBar ) { GetWindowRect( hStatBar, & rc ); minHeight += rc.bottom - rc.top; } if ( TRUE ) { TCHAR text[ 80 ]; sprintf( text, "%d x %d", minWidth, minHeight ); SetWindowText( hStatBar, text ); } }
解説
ドキュメントビューのクライアントエリアの最小値は固定です。
でも知りたいのはクライアントエリアではなくウィンドウサイズなので AdjustWindowRectEx() 関数で取得します。
これをメインウィンドウの必須クライアントエリアとして設定します。
次にツールバーとステータスバーが表示中ならば、これらの高さを追加します。
これが現時点でのウィンドウサイズの最小値となるんです。
デバッグのために計算結果をステータスバーに表示したりしてます。
その他
結果だけ書くと簡単だけど、うまく処理するのに要した試行錯誤量はハンパない。
サイズ変更メッセージへの対応は、ウィンドウの最大化のときと最小化のときとそれ以外で分けなきゃならないし。
サイズ変更時のちらつきを抑えるために設定値をいろいろ変えてみたり。
苦労した。
ドキュメントビュークラス
ドキュメントを何にするか考えましたが、とりあえずバイナリファイルにしました。
ビュワーなので編集はできませんが、その代わり漢字もちゃんと表示します。VisualStudio のバイナリエディタは漢字表示しないので。
class DocView : public View { BOOL FileNew(); BOOL FileOpen(); BOOL FileClose(); BOOL FileSave(); BOOL FileSaveAs(); BOOL Load(); BOOL Save(); LRESULT OnWmPaint(); };
解説
基底クラスを Window から View に変えました。スクロール対応は View が勝手にやっちゃってくれます。
なのでひたすらファイル処理と表示処理に専念します。
ファイルオープン
BOOL DocView :: FileOpen() { TCHAR path[ _MAX_PATH ]; OPENFILENAME ofn; GetPath( path ); ZeroMemory( & ofn, sizeof ofn ); ofn.lStructSize = sizeof ofn; ofn.lpstrFilter = filter; ofn.lpstrDefExt = defExt; ofn.lpstrFile = path; ofn.nMaxFile = sizeof path; ofn.Flags = OFN_CREATEPROMPT | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST; if ( GetOpenFileName( & ofn ) ) { FileClose(); strcpy( filePath, path ); if ( Load() ) { UpdateSize(); return TRUE; } } return FALSE; } BOOL DocView :: Load() { HANDLE hFile = CreateFile( filePath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL ); DWORD dwRead; if ( hFile != INVALID_HANDLE_VALUE ) { dwSize = GetFileSize( hFile, NULL ); buffer = HeapAlloc( GetProcessHeap(), 0, dwSize ); if ( buffer != NULL ) { ReadFile( hFile, buffer, dwSize, & dwRead, NULL ); CloseHandle( hFile ); return TRUE; } } return FALSE; }
解説
FileOpen() 関数でオープンするファイルを選択し Load() 関数でバッファに読み込みます。
単なるバイナリファイルなので何も考えずにバッファへ転送するだけです。
ファイル保存
BOOL DocView :: FileSave() { if ( strlen( filePath ) > 0 ) { return Save(); } return FileSaveAs(); } BOOL DocView :: FileSaveAs() { TCHAR path[ _MAX_PATH ]; OPENFILENAME ofn; if ( buffer == NULL ) { return TRUE; } GetPath( path ); ZeroMemory( & ofn, sizeof ofn ); ofn.lStructSize = sizeof ofn; ofn.lpstrFilter = filter; ofn.lpstrDefExt = defExt; ofn.lpstrFile = path; ofn.nMaxFile = sizeof path; #ifdef _DEBUG ofn.Flags = OFN_CREATEPROMPT | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST; #else ofn.Flags = OFN_CREATEPROMPT | OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT; #endif if ( GetSaveFileName( & ofn ) ) { strcpy( filePath, path ); return Save(); } return FALSE; } BOOL DocView :: Save() { HANDLE hFile = CreateFile( filePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); DWORD dwWrite; if ( hFile != INVALID_HANDLE_VALUE ) { WriteFile( hFile, buffer, dwSize, & dwWrite, NULL ); CloseHandle( hFile ); modify = FALSE; return TRUE; } return FALSE; }
解説
FileSave() 関数は現在のファイル名で保存、FileSaveAs() 関数は名前を付けて保存っちゅうことです。
表示
LRESULT DocView :: OnWmPaint() { PAINTSTRUCT ps; HDC hdc = BeginPaint( hwnd, & ps ); HBRUSH hbr = CreateSolidBrush( GetBkColor( hdc ) ); INT x = - GetScrollPos( hwnd, SB_HORZ ); INT y = - GetScrollPos( hwnd, SB_VERT ); int ch2 = 0; int ch; TCHAR text [ 256 ]; TCHAR text2[ 80 ]; RECT rc; GetClientRect( hwnd, & rc ); if ( buffer == NULL ) { FillRect( hdc, & rc, hbr ); } else { rc.left = fontSize.cx * textLen; FillRect( hdc, & rc, hbr ); SelectObject( hdc, hFont ); for ( DWORD addr = 0; addr < dwSize; addr += 16 ) { if ( y + fontSize.cy <= 0 ) { for ( DWORD offset = 0; offset < 16; offset++ ) { if ( addr + offset < dwSize ) { ch = ( ( LPBYTE ) buffer )[ addr + offset ]; if ( ch2 != 0 ) { ch2 = 0; } else if ( _ismbblead( ch ) ) { ch2 = ( ( LPBYTE ) buffer )[ addr + offset + 1 ]; } } } } else { sprintf( text, "%08X :", addr ); strcpy ( text2, " : " ); for ( DWORD offset = 0; offset < 16; offset++ ) { if ( offset == 8 ) { strcat( text, " -" ); } if ( addr + offset < dwSize ) { ch = ( ( LPBYTE ) buffer )[ addr + offset ]; sprintf( text + strlen( text ), " %02X", ch ); if ( ch2 != 0 ) { if ( offset == 0 ) { strcat( text2, " " ); } ch2 = 0; } else if ( _ismbblead( ch ) ) { ch2 = ( ( LPBYTE ) buffer )[ addr + offset + 1 ]; sprintf( text2 + strlen( text2 ), "%c%c", ch, ch2 ); } else if ( isprint( ch ) ) { sprintf( text2 + strlen( text2 ), "%c", ch ); } else { strcat( text2, "." ); } } else { strcat( text, " " ); strcat( text2, " " ); } } strcat( text, text2 ); strcat( text, " " ); TextOut( hdc, x, y, text, ( int ) strlen( text ) ); } if ( ( y += fontSize.cy ) >= ( rc.bottom - rc.top ) ) { break; } } rc.left = 0; rc.top = y; FillRect( hdc, & rc, hbr ); } DeleteObject( hbr ); EndPaint( hwnd, & ps ); return 0; }
解説
何気ないですが、ここにはちらつき予防策が施されております。
サイズ変更やスクロールなんかでインバリデートが発生するたびに背景を塗りつぶしていると、画面がちらついてしょうがありません。
そこでビュークラスの方ではクラス登録で wcx.hbrBackground = NULL とし、背景を塗りつぶしません。透明です。
ここのペイント処理の方で必要な分だけ塗りつぶしています。
あと表示領域外で TextOut() 関数をするとかなり遅くなるのでちゃんとスキップしています。