切入主题吧


打开超级马里奥1,选择工具->查看器->内存查看器,出现内容如下图1所示。

134009206.png

图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模式的结果是这样的:

162402872.png

图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()作用是反转像素。


此外这个窗口还有很多的消息响应函数,这节就不讲了。学以致用,如果以后要用到相关的知识,再回过头来研究一番吧。