切入主題吧
打開超級馬里奧1,選擇工具->查看器->內存查看器,出現內容如下圖1所示。
圖1
與內存查看器相關的類是CMemoryView,所在文件:
Source Files/MemoryView.cpp Header Files/MemoryView.h
該類的對象m_MemoryView聲明在CMainFrame類中。
以上內容是不是很相似啊,我複製了上節的內容,改了幾個關鍵字。。哈哈~~囧。
可以看到圖1顯示出的窗口界面還是比較複雜的,有點像二進制編輯器這類工具的界面。因此我打算這部分用兩節介紹。這一節是Win32編程方面,主要介紹這些數據是如何排版佈局顯示出來的;下一節是NES方面,主要介紹NES內存相關的內容。
Win32
CMemoryView::Create()
不考慮構造、析構函數的話,構建窗口的過程算是從這裏開始的。
代碼如下:
BOOL CMemoryView::Create( HWND hWndParent ) { m_logFont.lfCharSet = SHIFTJIS_CHARSET; if( !(m_hFont = ::CreateFontIndirect( &m_logFont )) ) { m_logFont.lfCharSet = ANSI_CHARSET; if( !(m_hFont = ::CreateFontIndirect( &m_logFont )) ) { return FALSE; } } HWND hWnd = ::CreateWindowEx( WS_EX_TOOLWINDOW, VIRTUANES_WNDCLASS, "MemoryView", WS_OVERLAPPEDWINDOW|WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWndParent, NULL, CApp::GetInstance(), (LPVOID)this ); if( !hWnd ) { DEBUGOUT( "CreateWindow faild.\n" ); return FALSE; } m_hWnd = hWnd; return TRUE; }
第3-9行 是在創建字體。m_logFont是LOGFONT結構。其默認值定義在MemoryView.cpp文件開頭部分。
LOGFONTCMemoryView::m_logFont={ FONTHEIGHT, FONTWIDTH, 0, 0, 0, FALSE, FALSE, FALSE, SHIFTJIS_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, FIXED_PITCH|FF_DONTCARE, NULL };
改變其中的任意一個值,都會影響字體的顯示效果。LOGFONT具體每個字段的含義可以在網上搜索"LOGFONT"查看。
第10行 CreateWindowEx調用時,Windows會自動調用CMemoryView::OnCreate()函數。
CMemoryView::OnCreate()
WNDMSG CMemoryView::OnCreate( WNDMSGPARAM ) { if( RCWIDTH(Config.general.rcMemoryViewPos) > 0 && RCHEIGHT(Config.general.rcMemoryViewPos) > 0 ) { ::MoveWindow( m_hWnd, Config.general.rcMemoryViewPos.left, Config.general.rcMemoryViewPos.top, RCWIDTH(Config.general.rcMemoryViewPos), RCHEIGHT(Config.general.rcMemoryViewPos), FALSE ); } else { RECT rw, rc; ::GetWindowRect( m_hWnd, &rw ); ::GetClientRect( m_hWnd, &rc ); INT x = rw.right - rw.left - rc.right + OFFSETH*2+FONTWIDTH*71; INT y = rw.bottom - rw.top - rc.bottom + OFFSETV*2+FONTHEIGHT*18; ::MoveWindow( m_hWnd, 0, 0, x, y, FALSE ); } RECT rc; ::GetClientRect( m_hWnd, &rc ); m_DispLines = (RCHEIGHT(rc)-(OFFSETV*2+FONTHEIGHT*2))/FONTHEIGHT; if( m_DispLines < 0 ) m_DispLines = 0; DEBUGOUT( "Display Lines:%d\n", m_DispLines ); DEBUGOUT( "Scroll Max :%d\n", (0xFFF-m_DispLines)<0?0:(0xFFF-m_DispLines) ); SCROLLINFO sif; ::ZeroMemory( &sif, sizeof(sif) ); sif.cbSize = sizeof(sif); sif.fMask = SIF_ALL; sif.nMin = 0; sif.nMax = (0x1000-m_DispLines); sif.nPos = 0; sif.nPage = 1; sif.nTrackPos = 1; ::SetScrollInfo( m_hWnd, SB_VERT, &sif, TRUE ); m_StartAddress = 0; m_CursorX = m_CursorY = 0; ::ShowWindow( m_hWnd, SW_SHOW ); ::SetTimer( m_hWnd, 1, 50, NULL ); return TRUE; }
代碼顯示出來很亂,大家也可以直接看源碼,我爲了說明方便,還是貼出來吧。
Config在以上代碼中出現了很多次。Config是CConfig類的一個全局對象,保存了各種的配置信息,比如遊戲窗口大小,用戶自定義按鍵等等。關於CConfig這個類,以後有機會可以詳細介紹一下。
在這裏,Config保存了內存查看器窗口的位置和大小。
第3-4行 內存查看器窗口的位置和大小是存儲在一個RECT結構中的。在該結構中儲存了該窗口的左上角和右下角座標。這條判斷語句即是判斷之前存儲的RECT結構是否有意義。
第6-9行 如果存儲的RECT結構有意義,則移動窗口到指定位置並確定大小。
第13-19行 在默認位置畫一個默認大小的窗口。
第21-23行 求算當前窗口大小適合顯示幾行字符。RCHEIGHT(rc)是窗口的客戶區高度,OFFSETV*2是最上和最下空出來的一小段距離,主要是爲了看起來能舒服一點,FONTHEIGHT*2是指第一行標題加上第二行分割線。
第29-38行 設置滾動條。
剩下幾行代碼進行了參數的初始化,這些參數的作用,用到了再做介紹。此外還啓動了一個定時器。
CMemoryView::OnTimer()
WNDMSG CMemoryView::OnTimer( WNDMSGPARAM ) { if( !Emu.IsRunning() ) return TRUE; HDC hDC = ::GetDC( m_hWnd ); OnDraw( hDC ); ::ReleaseDC( m_hWnd, hDC ); return TRUE; }
第3-4行 檢測是否有遊戲正在運行。
第5行 獲得窗口的設備句柄。
第7行 釋放設備句柄。
第6行 調用OnDraw函數使用獲得的hDC繪畫。
CMemoryView::OnDraw()
代碼行數比較多,我分開來貼。
第一部分代碼
void CMemoryView::OnDraw( HDC hDC ) { RECT rc; ::GetClientRect( m_hWnd, &rc ); HFONT hFontOld = (HFONT)::SelectObject( hDC, m_hFont ); ::SetBkMode( hDC, OPAQUE ); ::TextOut( hDC, OFFSETH, OFFSETV+FONTHEIGHT* 0, "ADDR +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F 0123456789ABCDEF", 71 ); ::TextOut( hDC, OFFSETH, OFFSETV+FONTHEIGHT* 1, "-----------------------------------------------------------------------", 71 );
第5行 使用之前定義的字體。
第6行 設置文字的背景顯示模式。OPAQUE模式下,每次都會用背景畫刷重新畫文字背景。還有一個TRANSPARENT模式,這個模式下只畫文字不畫背景,也就是說文字背景是透明的。在我們這個程序中,每次開始寫內存數據的時候,並沒有將之前畫的內容消除,所以只能用OPAQUE模式來覆蓋之前的內容。使用TRANSPARENT模式的結果是這樣的:
圖2
第7-10行 畫標題和分割線。
第二部分代碼
CHAR szBuf[256]; INT address = m_StartAddress; INT i; for( i = 0; i < m_DispLines; i++ ) { //顯示一行字符的代碼,等下補充 address += 16; address &= 0xFFFF; }
第1行 每次循環,szBuf都用來存儲對應行的字符。
第2行 m_StartAddress是內存查看器左上角的字節在字節數組中的下標。字節數組的意義在NES部分進行介紹。
第4行 循環顯示一行字符。
第6行 由於一行顯示16個字節,本行的第一個字節與下一行的第一個字節數組下標差了16。
第7行 應該是出於程序健壯性的考慮,通常情況下address不會大於0xFFFF
接下來是循環體的代碼。
::wsprintf( szBuf, "%04X ", address&0xFFFF ); for( INT d = 0; d < 16; d++ ) { CHAR szTemp[16]; INT addr = address+d; ::wsprintf( szTemp, "%02X ", CPU_MEM_BANK[addr>>13][addr&0x1FFF] ); ::strcat( szBuf, szTemp ); } ::strcat( szBuf, " " ); for( INT a = 0; a < 16; a++ ) { CHAR szTemp[16]; INT addr = address+a; if( m_logFont.lfCharSet == SHIFTJIS_CHARSET ) { ::wsprintf( szTemp, "%1c", ::_ismbcprint(CPU_MEM_BANK[addr>>13][addr&0x1FFF])?CPU_MEM_BANK[addr>>13][addr&0x1FFF]:'.' ); } else { ::wsprintf( szTemp, "%1c", ::isprint(CPU_MEM_BANK[addr>>13][addr&0x1FFF])?CPU_MEM_BANK[addr>>13][addr&0x1FFF]:'.' ); } ::strcat( szBuf, szTemp ); } ::TextOut( hDC, OFFSETH, OFFSETV+FONTHEIGHT*(2+i), szBuf, ::strlen(szBuf) );
第1行 寫入每一行數據首字節的數組下標。
第2-8行 寫入16個字節的數據。
第10-26行 lfCharSet是字符集。SHIFTJIS_CHARSET代表日文字符集。這段代碼意義如下,根據指定的字符集,找到每一個字節對應的字符。如果該字符能打印則直接保存,無法打印用‘.’表示。
第27-28行 將一行的字符顯示出來。
第三部分代碼
::TextOut( hDC, OFFSETH, OFFSETV+FONTHEIGHT*(2+i), " ", 71 ); if( m_DispLines ) { RECT rcInv; rcInv.left = OFFSETH+FONTWIDTH*6+FONTWIDTH*3*(m_CursorX>>1)+FONTWIDTH*(m_CursorX&1)-1; rcInv.top = OFFSETV+FONTHEIGHT*2+FONTHEIGHT*m_CursorY; rcInv.right = rcInv.left+FONTWIDTH; rcInv.bottom = rcInv.top+FONTHEIGHT; ::InvertRect( hDC, &rcInv ); rcInv.left = OFFSETH+FONTWIDTH*55+FONTWIDTH*(m_CursorX>>1)-1; rcInv.top = OFFSETV+FONTHEIGHT*2+FONTHEIGHT*m_CursorY; rcInv.right = rcInv.left+FONTWIDTH; rcInv.bottom = rcInv.top+FONTHEIGHT; ::InvertRect( hDC, &rcInv ); } ::SelectObject( hDC, hFontOld );
第1-2行 顯示完最後一行字符後,在下一行顯示一個全部爲空格的字符串。爲什麼要這樣呢?你可以試着註釋掉這行。運行程序,打開內存查看器,拉動窗口的底邊框,遮住最後一行字符的一部分。
第3-16行 m_CursorX是當前選中的字節的橫座標*2。m_CursorY是當前選中的字節的縱座標。這段代碼的作用是把選中的字節的左半部分和這個字節對應的字符一起改成黑底白字。5-9,11-14行這8個算式只要盯着圖1看一會兒,很快就能看明白的。InvertRect()作用是反轉像素。
此外這個窗口還有很多的消息響應函數,這節就不講了。學以致用,如果以後要用到相關的知識,再回過頭來研究一番吧。