Monday, May 31, 2004

 

Some notes on Dialog

1.
如何只UpdateData一个对话框的某一个或几个(不是全部)控件的值

算是知识漏洞吧,还是jiangsheng提供的代码片断,原文节选如下

void CDlgSalesBatchDetail::DoDataExchange(CDataExchange* pDX)
{

CBCGDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CDlgSalesBatchDetail)
DDX_Control(pDX, IDC_LIST_SPEC, m_listSpecCount);
//}}AFX_DATA_MAP

if(m_bUpdateAdoCombos)
{

DDX_Control(pDX, IDC_LIST, m_wndListPreview);
}

}

由于我自己写的DDX函数SalesHelper::DDX_AdoDataCombo可能比较慢(要查数据库),所以有时候不参与DDX,比如一个函数可能是这样

m_bUpdateAdoCombos=FALSE;
if(!UpdateData())
{
//更新数据
m_bUpdateAdoCombos=TRUE;
return;
}

//一个成员变量的值=另外两个成员变量的值之和
UpdateData(FALSE);
m_bUpdateAdoCombos=TRUE;//更新数据

整个过程和SalesHelper::DDX_AdoDataCombo关联的变量没有关系,就不用浪费时间去查数据库了。跑题一下,MSDN技术文章TN026: DDX and DDV Routines很不错的。

2.
如何在对话框中响应键盘按键

来源:VCKBASE

我们首先想到的是响应WM_KEYDOWN消息,但实际运行却发现没有任何效果。
原因是对话框里的控件需要首先对按键作出响应,比如多行编辑框必须首先
处理回车,不至于回车使对话框关闭。
我们要想在第一时间对对话框的按键做出响应,需要重载PreTranslateMessage,
以下的代码实现了在对话框中显示虚拟键值(virtual-key code)
BOOL CTestDlg::PreTranslateMessage(MSG* pMsg)
{
if(pMsg->message == WM_KEYDOWN)
{
///或者直接调用OnKeyDown
CString strwParam;
strwParam.Format("%d ",pMsg->wParam);
CDC* pDC = GetDC();
pDC->TextOut(10,10,strwParam);
ReleaseDC(pDC);
}
return CDialog::PreTranslateMessage(pMsg);
}

此方法也适用于FORMVIEW或其它控件中对键盘按键的响应,以下的代码来自MSDN,
实现了当按下上下左右方向键时调用OnKeyDown,我们可以在OnKeyDown中作相应处理。

BOOL CSampleControl::PreTranslateMessage(LPMSG lpmsg)
{
BOOL bHandleNow = FALSE;
switch (lpmsg->message)
{
case WM_KEYDOWN:
switch (lpmsg->wParam)
{
case VK_UP:
case VK_DOWN:
case VK_LEFT:
case VK_RIGHT:
bHandleNow = TRUE;
break;
}
if (bHandleNow)
OnKeyDown(lpmsg->wParam, LOWORD(lpmsg ->lParam), HIWORD(lpmsg->lParam));
break;
}
return bHandleNow;
}

(按:其实这个方法还可以用于以下地方。把一个按钮设置为OWNER DRAW,你就无法再用ENTER键去CLICK这个按钮了。这应该是MICROSOFT的一个BUG。但是我们可以用类似上面的方法处理
BOOL CDlg::PreTranslateMessage(MSG* pMsg)
{
if(pMsg->message == WM_KEYDOWN)
{
if(pMsg->wParam==VK_RETURN)
{
if(GetFocus()==GetDlgItem(IDC_BUTTON1))
{
OnButon1();
return 1;
}
}
return CDialog::PreTranslateMessage(pMsg);
})

Sunday, May 30, 2004

 

Some notes on Doc/View

1.
改变MFC默认文档操作方式

经过分析MFC源代码可知。其默认菜单中的"新建""打开"消息分别对应CwinApp::OnFileNew()与CwinApp::OnFileOpen(),而这两个函数又直接调用CDocManager::OnFileNew与CDocManager::OnFileOpen。CDocManager::OnFileOpen的工作过程如下:

1. 调用CDocManager:oPromptFileName,而DoPromptFileName的作用是显示文件打开对话框获得文件名。

2. 调用CWinApp::OpenDocumentFile(LPCTSTR lpszfileName)。

如果我们要改变文档打开方式,只需在CWinApp的继承类中重载DoPromptFileName与OpenDocumentFile即可。例如要改变默认的文件打开对话框。只需自建一个文件打开对话框,在重载的DoPromptFileName中调用他,获得一个文件名。

下面分析CWinApp::OpenDocumentFile。OpenDocumentFile的工作过程是:

1. 由参数lpszFileName的后缀判断用哪个文档模板。

2. 对该文档模板的的文档,视图,框架窗口作出调整。

3. 调用CDocTemplate::OpenDocumentFile

CDocTemplate::OpenDocumentFile是纯虚函数,由其派生类CsingleDocTemplate及CMultiDocTemplate实现,以CSingleDocTemplate::OpenDocumentFile举例说明之。

1. 判断有无现存文档,有则判断其是否已保存。

2. 新建框架窗口。

3. 由文件名路径判断该文件是否存在,存在则调用自定义的CMyDoc::OnOpenDocument,否则调用CMyDoc::OnNewDocument。

从以上分析我们可以对MFC默认的文档操作方式做多种改变,如改变文件打开保存方式,改变文件打开保存对话框等。

以上所示的各种源代码都可以从VC安装目录下的MFC\include及mfc\src子目录下得到。CWinApp类及CDocManager类CDocTemplate类及CsingleDocManager,CDocument类的头文件是afxwin.h,CWinApp类的源代码在AppCore.cpp中,CDocManager源代码在Docmgr.cpp中,CDocTemplate类及CSingleDocManager类的源代码分别位于doctempl.cpp与docsingl.cpp中。

具体应用参见

Changing default file open/save dialogs
Jorge Lodos Vigil August 7, 1998
http://www.codeguru.com/Cpp/W-D/dislog/commondialogs/article.php/c1967/

而下面的方法对SDI也可,不过不推荐

CDocument::DoSave revealed
By Nishant S

http://www.codeproject.com/docview/dosaverevealed.asp

Explains how you can suppress the File-Save-As dialog in a Doc/View app, how to save files to multiple formats, and how DoSave is implemented.

2.
关于"建立空文档失败"的问题的分析

作者:checkyvc6
出处:不详

许多新手在遇到此类问题时总是措手无策,如果谁有耐心就看看我写的下面这片文章吧。

这类问题的出现主要在BOOL CWinApp::ProcessShellCommand(CCommandLineInfo& rCmdInfo);

函数的关键内容:
BOOL bResult = TRUE;
switch (rCmdInfo.m_nShellCommand)
{
case CCommandLineInfo::FileNew: // 新建
if (!AfxGetApp()->OnCmdMsg(ID_FILE_NEW, 0, NULL, NULL))
OnFileNew();
if (m_pMainWnd == NULL)
bResult = FALSE;
break;
case CCommandLineInfo::FileOpen:
if (!OpenDocumentFile(rCmdInfo.m_strFileName))
bResult = FALSE;
break;
通过上面的内容我们可以看出:如果没有对ID_FILE_NEW做映射的话出现问题就在OnFileNew();
CWinApp对OnFileNew的默认实现是调用m_pDocManager->OnFileNew();

我们继续解析CDocManager,它究竟干了些什么?(首先说明一下CDocManager它主要的功能是帮助CWinApp是管理文档模板链表和注册文件类型.)

//如果模板列表为空的话
if (m_templateList.IsEmpty())
{
TRACE0("Error: no document templates registered with CWinApp.\n");
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC); //报错并返回.这里不会报建立新文档出错。
return;
}

CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead();
if (m_templateList.GetCount() > 1)
{
// more than one document template to choose from
// bring up dialog prompting user
CNewTypeDlg dlg(&m_templateList);
int nID = dlg.DoModal();
if (nID == IDOK)
pTemplate = dlg.m_pSelectedTemplate;
else
return; // none - cancel operation
}

ASSERT(pTemplate != NULL);
ASSERT_KINDOF(CDocTemplate, pTemplate);

pTemplate->OpenDocumentFile(NULL);

通过上面的代码我们可以看出,CWinApp的OnFileNew和OnFileOpen分别调用CDocManager的虚拟函数OnFileNew和OnFileOpen。而在CDocManager里面。通过模板链表选择不同的模板来调用文档模板的OpenDocumentFile();
如果传入参数NULL表示新建文件。

下面我们来看看CDocTemplate::OpenDocumentFile()它是一个最关键的函数。因为他是虚拟函数,我们考虑CSingleDocTemplate::OpenDocumentFile的情况。
这个函数里面有一段代码:
其中:AFX_IDP_FAILED_TO_CREATE_DOC 就是字符“建立空文档失败”的资源id
// create a new document
pDocument = CreateNewDocument();
ASSERT(pFrame == NULL); // will be created below
bCreated = TRUE;
if (pDocument == NULL)
{
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);
return NULL;
}
ASSERT(pDocument == m_pOnlyDoc);
if (pFrame == NULL)
{
ASSERT(bCreated);

// create frame - set as main document frame
BOOL bAutoDelete = pDocument->m_bAutoDelete;
pDocument->m_bAutoDelete = FALSE;
// don't destroy if something goes wrong
pFrame = CreateNewFrame(pDocument, NULL);
pDocument->m_bAutoDelete = bAutoDelete;
if (pFrame == NULL)
{
AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);
delete pDocument; // explicit delete on error
return NULL;
}

通过观察上面的代码我们很容易的看出 有两个可能出错的原因:1 CreateNewDocument返回为NULL; 2 CreateNewFrame 返回为空。

先看 CreateNewDocument() 一般来说这个函数很少失败。不过在调试时也不能掉以轻心。
再看看CreateNewFrame() 里面有一个函数LoadFrame是造成这种“建立新文档失败”错误的源泉所在。只要它返回False就会弹出这样的提示。
我们再来看看LoadFrame() 里面调用CFrameWnd::Create()来创建窗口,创建窗口失败返回Fasle。这样问题就变的比较简单了。

看看Create和CreateEx函数的动作就知道怎么回事了。
****************************************************************
1 如果找不到菜单资源 返回False 同时也弹出“建立空文档失败”
HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU);
if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL)
{
TRACE0("Warning: failed to load menu for CFrameWnd.\n");
PostNcDestroy(); // perhaps delete the C++ object
return FALSE;
}
2 重载了PreCreateWindow而且返回False也会导致弹出“建立空文档失败”
3 在OnCreate 里面返回-1 也会导致弹出“建立空文档失败”。
******************************************************************

以上就是我分析的 出现这样“建立空文档失败”问题的大致原因。也许还有其他的原因。我这里就不一一列举了。

3.
UpdateAllViews的用法

作者:danny ye
出处:http://www.vchelp.net/cndevforum/subject_view.asp?subject_id=1025&forum_id=22

首先考虑几点:
首先检查GetDocument是否指向你的Doc(看看是不是CMyDoc* Getdocument()),而不是CDocument* Getxxxxx().
其次在调试状态检查你的Doc是否与你所有的View关联。
你可以用pDoc->GetFirstViewPosition和pDoc->GetNextView遍历所有的视,检查一下视列表中是否包含你要处理的视。一般是通过文档模板和
切分窗口中用CreateView加入到Doc的视列表中。SDI的多视一般是用
切分窗口或CTabCtrl方法实现的,你看看是否正确,没有就用pDoc->AddView做或者干脆用AddDocTemplate做成单文档多模板的方式。

其次this和NULL的差别在于NULL是更新所有视而this是除自己(发送UpdateAllView的视)外其余的都更新,这样系统认为是你自己(View)已经更新过了,你的问题跟这没关系,问题在于其他视没有和你的Doc挂钩.因为UpdateAllView中就是通过遍历视列表调用所有视的OnUpdate,如果视列表中所有的View都在,那一定是OnUpdate处直接返回了,而没有缺省调用父类的OnUpdate.

最后总结:
if (thisView->OnUpdate根本没执行)
{
//看原代码知道pDoc->UpdateAllView中是根据的视列表中包含的视来执行相应的OnUpdate
推断:m_pViewClass中没有包含你的View.
解决:1.用new构造View后,用pDoc->AddView加入;
本方法在CTabCtrl管理的多视时常用;
2。在框架的OnCreateClient的地方(用切分窗口时一样), 会用到CreateView,
CCreateContext是OnCreateClient的参数头来传递,
你可以不必关心细节(如CCreateContext中的m_pCurrentDoc),因为框架已经与文档关联了(在AddDocTemplate时,)MFC知道如何传递正确的CCreateContext,View就是在这种情况下
加入Doc的View列表中;
3。用AddDocTemplate(即多模板的方法),太难看,我不喜欢,不推荐。
4。Camel说的方法,自己处理CCreateContext来创建与文档关联的View(不仅仅是构造一个View). 你的View就可以加入到m_pViewClass中.好处是可在你想创建视的地方,不必是OnCreateClient等有CCreateContext参数头的地方,即可以在需要的时候动态创建,唯一的风险就是m_pCurrentDoc在MFC升级后会不会变(很多书对类似情况都这么说,不推荐直接访问成员变量,而用成员函数访问)。
}
else
{
OnUpdate里没有调用父类的OnUpdate或者没有Invalidate,InvalidateRect之类的触发View重画的函数。
}

4.
直接处理MRU
http://www.codeproject.com/docview/most_recent_used.asp

改写以前的程序不得已用过一次,还行。不过,同时注意Eat相关的消息
By Ramil C. Matira
From http://www.codeproject.com/docview/most_recent_used.asp#xx993102xx

To enable action on MRU you must add the following code. The ID_FILE_MRU_FILE1..ID_FILE_MRU_FILEx are actually the menu ids of the MRU's:

On MainFrm.cpp

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
ON_COMMAND_RANGE(ID_FILE_MRU_FILE1, ID_FILE_MRU_FILE4, OnFileMruFile)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
...

void CMainFrame::OnFileMruFile(UINT nID)
{
int nMRUIndex = -1;
switch (nID) {
case ID_FILE_MRU_FILE1:
nMRUIndex = 0;
break;
case ID_FILE_MRU_FILE2:
nMRUIndex = 1;
break;
case ID_FILE_MRU_FILE3:
nMRUIndex = 2;
break;
case ID_FILE_MRU_FILE4:
nMRUIndex = 3;
break;
}

((CCMRUTestApp*)AfxGetApp())->OpenRecentFileList(nMRUIndex);

}

On CMRUTest

void CCMRUTestApp::OpenRecentFileList(int nIndex)
{
if (m_pRecentFileList != NULL) {
CString strFilename;

if (m_pRecentFileList->GetDisplayName(strFilename,
nIndex,
_T("."),
1))
AfxMessageBox(strFilename);
}
}
You must add code to determine the current directory and replece the _T(".") with its value

By Adam Solesby August 6, 1998
From http://www.codeguru.com/Cpp/W-D/doc_view/mrumostrecentusedfilelist/article.php/c3293/


This is very simple, but I had never seen it anywhere else until I had to do it myself. It should save everyone some digging through the documentation.

To make your program re-open the most recently used file, simply add the following code to your App's InitInstance() between the calls to ParseCommandLine() and ProcessShellCommand().


// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);

// ====================== Begin Inserted code ==============================
// If a file is not specified on the command line, open the last file
if ( ! cmdInfo.m_strFileName.GetLength() )
{
CString strFileName;
if (m_pRecentFileList->GetDisplayName(strFileName, 0, "", 0, true))
{
cmdInfo.m_strFileName = strFileName;
cmdInfo.m_nShellCommand =
CCommandLineInfo::FileOpen;
}
}
// ====================== End Inserted code ==============================


// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;


(continued)




Nico van Ravenstein adds:

There is a problem with opening the most recent file when the filepath is too long. The directory gets abbreviated by the function AbbreviateName like C:\some path\...\file.txt. Thats nice for a windows title or in the last used list but you can't open a file with it.

So a better way is this:

// Add this to your applications InitInstance function
//
// If a file is not specified on the command line, open the last file
if (!cmdInfo.m_strFileName.GetLength())
{
if (m_pRecentFileList->m_nSize > 0 &&
!m_pRecentFileList->m_arrNames[0].IsEmpty())
{
cmdInfo.m_strFileName = m_pRecentFileList->m_arrNames[0];
cmdInfo.m_nShellCommand = CCommandLineInfo::FileOpen;
}
}

5.
How to switch multi-views

5.1
怎样在一个Pane中显示多种View?
2000-01-30 00:00:00· 祁文文·CPCW

---- 在MS Windows 中,一个窗口可以分割成若干个子窗口,每一个子窗口称作一个窗片(pane),每个窗片可以独立控制,这给界面设计提供了很大的方便。

---- 利用VC 可以很方便地实现分割窗口。分割的方法有两种:动态和静态。动态分割时可以根据用户的需要分割成数目不同的窗片,但所有窗片的属性和父窗口都是一样的;而静态分割的窗片的数目在程序中指定,运行时是固定的,但每个窗片可以有各自不同类型的视(View),因此其使用范围更为广泛。本文所讨论的问题仅限于静态分割。

---- 窗片中视的类型大多是在主窗口的创建过程中指定的。这也就意味着,一个窗片虽然可以显示任意类型的视,但是这种类型一旦确定,在程序运行过程中就难以改变。

---- 一、我要的是这样的!

---- 但是我们有时确实需要改变一个窗片所显示的视的类型,也就是说,需要让一个窗片显示多种类型的视。例如一个窗口被分割成两部分,一边是命令窗口,另一边是工作窗口,根据命令窗口中发出的不同命令,需要变换不同的工作类型,这就需要工作窗口中能够显示多种类型的视窗,那么,如何做到这一点呢?

---- 二、你可以这样做!

---- 从图1 中可以看到,本程序共有三个视类,分别是:

---- ? 命令视类CCmdView:用来控制右边窗片中不同视的显示;

---- ? 选项按钮视类CRdiView:显示在右窗片中的选项视类;

---- ? 检查按钮视类CChkView:显示在右窗片中的检查视类。

---- 这三个视类都是CFormView 的子类。

---- 下面我们来看如何在右窗片内进行两类视间的切换。实际上,由视A 切换到视B 的原理很简单,那就是:

---- 1. 从窗片中删除视A;

---- 2. 往窗片中添加视B。

---- 步骤1 的实现非常简单,仅用一条语句即可:

---- m_wndSplitter.DeleteView(0, 1);

---- 但它是必不可少的,因为你不能让一个窗片同时包含两个视。我本来希望往一个窗片中添加新的视时,VC 会自动将原来的视删掉,可是它不干。

---- 我们来看如何实现步骤2,当一个窗片是空的时候,怎样往里面添加一个视呢?其实这样的功能在程序里我们已经用过了,看下面的语句:

BOOL CMainFrame::OnCreateClient
(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
……
if (!m_wndSplitter.CreateView(0, 0,
pContext->m_pNe ewClass,
size,
pContext))
……
}
---- 是的,用的就是CSplitterWnd::CreateView(),要注意的是它共有五个参数,其中前两个用来指定分割窗口的窗片,第三个用来指定视的类型,第四个指定视的大小。最后的一个我们暂时用不上,用空值NULL 就可以了。

---- 这样我们就可以编写视切换的代码了。因为视切换要操纵m_wndSplitter,而它是主窗口的成员,因此切换过程最好设计为主窗口的成员函数。但是切换命令是CCmdView 接受的,因而可以让CCmdView 接受到视更改消息后,将消息传给主窗口,由主窗口完成视更改。具体的代码是这样的:

---- 命令视类中的消息映射:

BEGIN_MESSAGE_MAP(CCmdView, CFormView)
……
ON_BN_CLICKED(IDC_CHECK, OnSwitchToCheckView)
ON_BN_CLICKED(IDC_RADIO, OnSwitchToRadioView)
……
END_MESSAGE_MAP()

命令视类中的消息响应:
void CCmdView::OnSwitchToCheckView()
{
AfxGetApp()->m_pMainWnd->
SendMessage(WM_COMMAND, ID_CHECK);
}

void CCmdView::OnSwitchToRadioView()
{
AfxGetApp()->m_pMainWnd->
SendMessage(WM_COMMAND, ID_RADIO);
}

主窗口中的消息映射:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
……
ON_COMMAND(ID_CHECK, OnSwitchToCheckView)
ON_COMMAND(ID_RADIO, OnSwitchToRadioView)
……
END_MESSAGE_MAP()

主窗口中的消息响应:
void CMainFrame::OnSwitchToCheckView()
{
m_wndSplitter.DeleteView(0, 1);
m_wndSplitter.CreateView(0, 1,
RUNTIME_CLASS(CChkView),
CSize(0, 0),
NULL);
m_wndSplitter.RecalcLayout();
}

void CMainFrame::OnSwitchToRadioView()
{
m_wndSplitter.DeleteView(0, 1);
m_wndSplitter.CreateView(0, 1,
RUNTIME_CLASS(CRdiView),
CSize(0, 0),
NULL);
m_wndSplitter.RecalcLayout();
}
---- 好啦,运行一下这个程序,感觉是否不错?看来大功告成了,可是……

---- 三、还有一个问题

---- 在运行我们辛辛苦苦编出来的程序时,回头看看VC 的调试窗口,你会发现有很多行这样的话:

---- Create view without document.

---- 这是说我们创建了视,可是没有相应的文档。好在这只是警告信息,不是什么错误,如果你不需要相应的文档,就完全不用去管它。可是,VC 中一种很重要的结构就是文档- 视结构,利用这种结构,对数据操纵起来非常方便。如果需要建立与视相对应的文档,应该怎么办呢?

---- 这就涉及到VC 中文档- 视结构的知识,不过不用怕麻烦,与本文有关的就只有这么两点而已:

---- 1. 利用VC 创建的应用程序一般都会管理一些文档模板(Document Template),文档类和视类的对应关系就是在文档模板里描述的。

---- 2. 一个文档可以有多个视,创建视的时候,需要根据文档和视的对应关系,给出它所依附的文档。

---- 怎样实现上述第一点呢?

---- 首先建立相应的文档类:CRdiDoc 和CChkDoc。

---- 其次是定义相应的文档模板,这是应用类的成员变量。因为在别的类中要使用它们,我们将之定义为公共类型:

class CViewSwitcherApp : public CWinApp
{
……
public:
CSingleDocTemplate* m_pRdiDocTemplate;
CSingleDocTemplate* m_pChkDocTemplate;
……
}
然后创建这两个文档模板,并加入到模板列表中:
BOOL CViewSwitcherApp::InitInstance()
{
……
m_pRdiDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CRdiDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CRdiView));
AddDocTemplate(m_pRdiDocTemplate);

m_pChkDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CChkDoc),
RUNTIME_CLASS(CMainFrame),
RUNTIME_CLASS(CChkView));
AddDocTemplate(m_pChkDocTemplate);
……
}
---- 至于第二点,是在创建视时完成的。还记得创建视的情况么?当时有一个叫做pCreateContext 的参数,我们将之置为空,这里就要用到它了。

---- pCreateContext 是一个指向被称作" 创建上下文"(CreateContext) 结构的指针,这个结构中保存一些与创建视相关的内容。在创建主窗口时,系统会构造这样一个结构,并将它作为参数传递到与创建视有关的函数中。但现在我们不创建主窗口,因此不得不自己构造这样一个结构。实际上,该结构中我们所要使用的字段只有三个:

---- 1. 新视所属的文档模板m_pNewDocTemplate;

---- 2. 新视的类型m_pNewViewClass;

---- 3. 新视所属的文档m_pCurrentDoc;

---- 其中仅有第三项需要新建,前两项都是已知的,只要指定即可。以切换到选项视为例,修改后的代码是:

void CMainFrame::OnSwitchToRadioView()
{
m_wndSplitter.DeleteView(0, 1);

CCreateContext createContext;
// 定义并初始化CreateContext
// 获取新视所属的文档模板
CSingleDocTemplate* pDocTemplate =
((CViewSwitcherApp*)AfxGetApp())-> m_pRdiDocTemplate;
// 创建新文档并初始化
CDocument* pDoc = pDocTemplate->CreateNewDocument();
pDoc->OnNewDocument();

// 设置CreateContext 相关字段
createContext.m_pNewViewClass = RUNTIME_CLASS(CChkView);
createContext.m_pCurrentDoc = pDoc;
createContext.m_pNewDocTemplate = pDocTemplate;

m_wndSplitter.CreateView(0, 1,
RUNTIME_CLASS(CRdiView),
CSize(0, 0),
&createContext);

m_wndSplitter.RecalcLayout();
}
---- 四、最后的修改

---- 为了使这个程序更符合要求,我们还要做一些与更换视无关的修改。在这个程序中我们一共定义了三种类型的文档,程序启动时一般要新建一个文档开始工作,可是它不知道要选择哪一种,就弹出一个对话框来询问。而这是我们不希望看到的。修改的方法是不让VC 选择新文档类型,而我们指定创建哪一种类型的文档,即把CViewSwitcherApp::CViewSwitcherApp() 中的语句

---- if (!ProcessShellCommand(cmdInfo)) return FALSE;

---- 更改为

---- m_pDocTemplate->OpenDocumentFile(NULL)。

5.2
Replacing a view in a doc-view application
By Jorge Lodos Vigil

http://www.codeproject.com/docview/ReplacingView.asp

Saturday, May 29, 2004

 

How to call Java in C/C++

1.
作者:刘冬
出处:http://tech.ccidnet.com/pub/disp/Article?columnID=294&articleID=38068&pageNO=1

Java跨平台的特性使Java越来越受开发人员的欢迎,但也往往会听到不少的抱怨:用Java开发的图形用户窗口界面每次在启动的时候都会跳出一个控制台窗口,这个控制台窗口让本来非常棒的界面失色不少。怎么能够让通过Java开发的GUI程序不弹出Java的控制台窗口呢?其实现在很多流行的开发环境例如JBuilder、Eclipse都是使用纯Java开发的集成环境。这些集成环境启动的时候并不会打开一个命令窗口,因为它使用了JNI(Java Native Interface)的技术。通过这种技术,开发人员不一定要用命令行来启动Java程序,可以通过编写一个本地GUI程序直接启动Java程序,这样就可避免另外打开一个命令窗口,让开发的Java程序更加专业。

JNI允许运行在虚拟机的Java程序能够与其它语言(例如C和C++)编写的程序或者类库进行相互间的调用。同时JNI提供的一整套的API,允许将Java虚拟机直接嵌入到本地的应用程序中。

本文将介绍如何在C/C++中调用Java方法,并结合可能涉及到的问题介绍整个开发的步骤及可能遇到的难题和解决方法。本文所采用的工具是Sun公司创建的 Java Development Kit (JDK) 版本 1.3.1,以及微软公司的Visual C++ 6开发环境。


环境搭建

为了让本文以下部分的代码能够正常工作,我们必须建立一个完整的开发环境。首先需要下载并安装JDK 1.3.1,其下载地址为“http://java.sun.com”。假设安装路径为C:\JDK。

将目录C:\JDK\include和C:\JDK\include\win32加入到开发环境的Include Files目录中,同时将C:\JDK\lib目录添加到开发环境的Library Files目录中。这三个目录是JNI定义的一些常量、结构及方法的头文件和库文件。集成开发环境已经设置完毕,同时为了执行程序需要把Java虚拟机所用到的动态链接库所在的目录C:\JDK \jre\bin\classic设置到系统的Path环境变量中。这里需要提出的是,某些开发人员为了方便直接将JRE所用到的DLL文件直接拷贝到系统目录下。这样做是不行的,将导致初始化Java虚拟机环境失败(返回值-1),原因是Java虚拟机是以相对路径来寻找所用到的库文件和其它一些相关文件的。至此整个JNI的开发环境设置完毕,为了让此次JNI旅程能够顺利进行,还必须先准备一个Java类。在这个类中将用到Java中几乎所有有代表性的属性及方法,如静态方法与属性、数组、异常抛出与捕捉等。我们定义的Java程序(Demo.java)如下,本文中所有的代码演示都将基于该Java程序,代码如下:

[code]
package jni.test;
/**
* 该类是为了演示JNI如何访问各种对象属性等
* @author liudong
*/
public class Demo {
//用于演示如何访问静态的基本类型属性
public static int COUNT = 8;
//演示对象型属性
public String msg;
private int[] counts;
public Demo() {
this("缺省构造函数");
}
/**
* 演示如何访问构造器
*/
public Demo(String msg) {
System.out.println(":" + msg);
this.msg = msg;
this.counts = null;
}
/**
* 该方法演示如何访问一个访问以及中文字符的处理
*/
public String getMessage() {
return msg;
}
/**
* 演示数组对象的访问
*/
public int[] getCounts() {
return counts;
}
/**
* 演示如何构造一个数组对象
*/
public void setCounts(int[] counts) {
this.counts = counts;
}
/**
* 演示异常的捕捉
*/
public void throwExcp() throws IllegalAccessException {
throw new IllegalAccessException("exception occur.");
}
}
[/code]

初始化虚拟机

本地代码在调用Java方法之前必须先加载Java虚拟机,而后所有的Java程序都在虚拟机中执行。为了初始化Java虚拟机,JNI提供了一系列的接口函数Invocation API。通过这些API可以很方便地将虚拟机加载到内存中。创建虚拟机可以用函数 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。对于这个函数有一点需要注意的是,在JDK 1.1中第三个参数总是指向一个结构JDK1_ 1InitArgs, 这个结构无法完全在所有版本的虚拟机中进行无缝移植。在JDK 1.2中已经使用了一个标准的初始化结构JavaVMInitArgs来替代JDK1_1InitArgs。下面我们分别给出两种不同版本的示例代码。

在JDK 1.1初始化虚拟机:

[code]
#include
int main() {
JNIEnv *env;
JavaVM *jvm;
JDK1_1InitArgs vm_args;
jint res;
/* IMPORTANT: 版本号设置一定不能漏 */
vm_args.version = 0x00010001;
/*获取缺省的虚拟机初始化参数*/
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* 添加自定义的类路径 */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/*设置一些其他的初始化参数*/
/* 创建虚拟机 */
res = JNI_CreateJavaVM(&jvm,&env,&vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
/*释放虚拟机资源*/
(*jvm)->DestroyJavaVM(jvm);
}
[/code]

JDK 1.2初始化虚拟机:

[code]
/* invoke2.c */
#include
int main() {
int res;
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
vm_args.version=JNI_VERSION_1_2;//这个字段必须设置为该值
/*设置初始化参数*/
options[0].optionString = "-Djava.compiler=NONE";
options[1].optionString = "-Djava.class.path=.";
options[2].optionString = "-verbose:jni"; //用于跟踪运行时的信息
/*版本号设置不能漏*/
vm_args.version = JNI_VERSION_1_2;
vm_args.nOptions = 3;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_TRUE;
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
(*jvm)->DestroyJavaVM(jvm);
fprintf(stdout, "Java VM destory.\n");
}
[/code]

为了保证JNI代码的可移植性,建议使用JDK 1.2的方法来创建虚拟机。JNI_CreateJavaVM函数的第二个参数JNIEnv *env,就是贯穿整个JNI始末的一个参数,因为几乎所有的函数都要求一个参数就是JNIEnv *env。

访问类方法

初始化了Java虚拟机后,就可以开始调用Java的方法。要调用一个Java对象的方法必须经过几个步骤:

1.获取指定对象的类定义(jclass)

有两种途径来获取对象的类定义:第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:

jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠

然后通过对象直接得到其所对应的类定义:

jclass cls = (*env)-> GetObjectClass(env, obj);
//其中obj是要引用的对象,类型是jobject

2.读取要调用方法的定义(jmethodID)

我们先来看看JNI中获取方法定义的函数:

jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name,
const char *sig);
jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char
*name, const char *sig);

这两个函数的区别在于GetStaticMethodID是用来获取静态方法的定义,GetMethodID则是获取非静态的方法定义。这两个函数都需要提供四个参数:env就是初始化虚拟机得到的JNI环境;第二个参数class是对象的类定义,也就是第一步得到的obj;第三个参数是方法名称;最重要的是第四个参数,这个参数是方法的定义。因为我们知道Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要第四个参数来指定方法的具体定义。但是怎么利用一个字符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的定义。下面就来看看jni.test.Demo的定义:

打开命令行窗口并运行 javap -s -p jni.test.Demo 得到运行结果如下:

[code]
Compiled from Demo.java
public class jni.test.Demo extends java.lang.Object {
public static int COUNT;
/* I */
public java.lang.String msg;
/* Ljava/lang/String; */
private int counts[];
/* [I */
public jni.test.Demo();
/* ()V */
public jni.test.Demo(java.lang.String);
/* (Ljava/lang/String;)V */
public java.lang.String getMessage();
/* ()Ljava/lang/String; */
public int getCounts()[];
/* ()[I */
public void setCounts(int[]);
/* ([I)V */
public void throwExcp() throws java.lang.IllegalAccessException;
/* ()V */
static {};
/* ()V */
}
[/code]

我们看到类中每个属性和方法下面都有一段注释。注释中不包含空格的内容就是第四个参数要填的内容(关于javap具体参数请查询JDK的使用帮助)。下面这段代码演示如何访问jni.test.Demo的getMessage方法:

[code]
/*
假设我们已经有一个jni.test.Demo的实例obj
*/
jmethodID mid;
jclass cls = (*env)-> GetObjectClass (env, obj); //获取实例的类定义
mid=(*env)->GetMethodID(env,cls,"getMessage"," ()Ljava/lang/String; ");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallObjectMethod(env, obj, mid);
/*
如果该方法是静态的方法那只需要将最后一句代码改为以下写法即可:
jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid);
*/
[/code]

3.调用方法

为了调用对象的某个方法,可以使用函数CallMethod或者CallStaticMethod(访问类的静态方法),根据不同的返回类型而定。这些方法都是使用可变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。

访问类属性

访问类的属性与访问类的方法大体上是一致的,只不过是把方法变成属性而已。

1.获取指定对象的类(jclass)

这一步与访问类方法的第一步完全相同,具体使用参看访问类方法的第一步。

2.读取类属性的定义(jfieldID)

在JNI中是这样定义获取类属性的方法的:

jfieldID (JNICALL *GetFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID (JNICALL *GetStaticFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig);

这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数同样是为了表达属性的类型。前面我们使用javap工具获取类的详细定义的时候有这样两行:

public java.lang.String msg;
/* Ljava/lang/String; */

其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。

3.读取和设置属性值

有了属性的定义要访问属性值就很容易了。有几个方法用来读取和设置类的属性,它们是:GetField、SetField、GetStaticField、SetStaticField。比如读取Demo类的msg属性就可以用GetObjectField,而访问COUNT用GetStaticIntField,相关代码如下:

jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;");
jstring msg = (*env)-> GetObjectField(env, cls, field); //msg就是对应Demo的msg
jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I");
jint count = (*env)->GetStaticIntField(env,cls,field2);

访问构造函数

很多人刚刚接触JNI的时候往往会在这一节遇到问题,查遍了整个jni.h看到这样一个函数NewObject,它应该是可以用来访问类的构造函数。但是该函数需要提供构造函数的方法定义,其类型是jmethodID。从前面的内容我们知道要获取方法的定义首先要知道方法的名称,但是构造函数的名称怎么来填写呢?其实访问构造函数与访问一个普通的类方法大体上是一样的,惟一不同的只是方法名称不同及方法调用时不同而已。访问类的构造函数时方法名必须填写“”。下面的代码演示如何构造一个Demo类的实例:

[code]
jclass cls = (*env)->FindClass(env, "jni/test/Demo");
/**
首先通过类的名称获取类的定义,相当于Java中的Class.forName方法
*/
if (cls == 0)

jmethodID mid = (*env)->GetMethodID(env,cls,"","(Ljava/lang/String;)V ");
if(mid == 0)

jobject demo = jenv->NewObject(cls,mid,0);
/**
访问构造函数必须使用NewObject的函数来调用前面获取的构造函数的定义
上面的代码我们构造了一个Demo的实例并传一个空串null
*/
[/code]

数组处理

创建一个新数组

要创建一个数组,我们首先应该知道数组元素的类型及数组长度。JNI定义了一批数组的类型jArray及数组操作的函数NewArray,其中就是数组中元素的类型。例如,要创建一个大小为10并且每个位置值分别为1-10的整数数组,编写代码如下:

[code]
int i = 1;
jintArray array; //定义数组对象
(*env)-> NewIntArray(env, 10);
for(; i<= 10; i++)
(*env)->SetIntArrayRegion(env, array, i-1, 1, &i);
[/code]

访问数组中的数据

访问数组首先应该知道数组的长度及元素的类型。现在我们把创建的数组中的每个元素值打印出来,代码如下:

[code]
int i;
/* 获取数组对象的元素个数 */
int len = (*env)->GetArrayLength(env, array);
/* 获取数组中的所有元素 */
jint* elems = (*env)-> GetIntArrayElements(env, array, 0);
for(i=0; i< len; i++)
printf("ELEMENT %d IS %d\n", i, elems[i]);
[/code]

中文字符的处理往往是让人比较头疼的事情,特别是使用Java语言开发的软件,在JNI这个问题更加突出。由于Java中所有的字符都是Unicode编码,但是在本地方法中,例如用VC编写的程序,如果没有特殊的定义一般都没有使用Unicode的编码方式。为了让本地方法能够访问Java中定义的中文字符及Java访问本地方法产生的中文字符串,我定义了两个方法用来做相互转换。

· 方法一,将Java中文字符串转为本地字符串
[code]
/**
第一个参数是虚拟机的环境指针
第二个参数为待转换的Java字符串定义
第三个参数是本地存储转换后字符串的内存块
第三个参数是内存块的大小
*/
int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len)
{
int len = 0;
if(desc==NULL||str==NULL)
return -1;
//在VC中wchar_t是用来存储宽字节字符(UNICODE)的数据类型
wchar_t *w_buffer = new wchar_t[1024];
ZeroMemory(w_buffer,1024*sizeof(wchar_t));
//使用GetStringChars而不是GetStringUTFChars
wcscpy(w_buffer,env->GetStringChars(str,0));
env->ReleaseStringChars(str,w_buffer);
ZeroMemory(desc,desc_len);
//调用字符编码转换函数(Win32 API)将UNICODE转为ASCII编码格式字符串
//关于函数WideCharToMultiByte的使用请参考MSDN
len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL);
//len = wcslen(w_buffer);
if(len>0 && len desc[len]=0;
delete[] w_buffer;
return strlen(desc);
}
[/code]

· 方法二,将C的字符串转为Java能识别的Unicode字符串
[code]
jstring NewJString(JNIEnv* env,LPCTSTR str)
{
if(!env || !str)
return 0;
int slen = strlen(str);
jchar* buffer = new jchar[slen];
int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),buffer,slen);
if(len>0 && len < slen)
buffer[len]=0;
jstring js = env->NewString(buffer,len);
delete [] buffer;
return js;
}
[/code]

异常

由于调用了Java的方法,因此难免产生操作的异常信息。这些异常没有办法通过C++本身的异常处理机制来捕捉到,但JNI可以通过一些函数来获取Java中抛出的异常信息。之前我们在Demo类中定义了一个方法throwExcp,下面将访问该方法并捕捉其抛出来的异常信息,代码如下:

[code]
/**
假设我们已经构造了一个Demo的实例obj,其类定义为cls
*/
jthrowable excp = 0; /* 异常信息定义 */
jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V");
/*如果mid为0表示获取方法定义失败*/
jstring msg = (*env)-> CallVoidMethod(env, obj, mid);
/* 在调用该方法后会有一个IllegalAccessException的异常抛出 */
excp = (*env)->ExceptionOccurred(env);
if(excp){
(*env)->ExceptionClear(env);
//通过访问excp来获取具体异常信息
/*
在Java中,大部分的异常信息都是扩展类java.lang.Exception,因此可以访问excp的toString
或者getMessage来获取异常信息的内容。访问这两个方法同前面讲到的如何访问类的方法是相同的。
*/
}
[/code]

线程和同步访问

有些时候需要使用多线程的方式来访问Java的方法。我们知道一个Java虚拟机是非常消耗系统的内存资源,差不多每个虚拟机需要内存大约在20MB左右。为了节省资源要求每个线程使用的是同一个虚拟机,这样在整个的JNI程序中只需要初始化一个虚拟机就可以了。所有人都是这样想的,但是一旦子线程访问主线程创建的虚拟机环境变量,系统就会出现错误对话框,然后整个程序终止。

其实这里面涉及到两个概念,它们分别是虚拟机(JavaVM *jvm)和虚拟机环境(JNIEnv *env)。真正消耗大量系统资源的是jvm而不是env,jvm是允许多个线程访问的,但是env只能被创建它本身的线程所访问,而且每个线程必须创建自己的虚拟机环境env。这时候会有人提出疑问,主线程在初始化虚拟机的时候就创建了虚拟机环境env。为了让子线程能够创建自己的env,JNI提供了两个函数:AttachCurrentThread和DetachCurrentThread。下面代码就是子线程访问Java方法的框架:

[code]
DWORD WINAPI ThreadProc(PVOID dwParam)
{
JavaVM jvm = (JavaVM*)dwParam; /* 将虚拟机通过参数传入 */
JNIEnv* env;
(*jvm)-> AttachCurrentThread(jvm, (void**)&env, NULL);
.........
(*jvm)-> DetachCurrentThread(jvm);
}
[/code]

时间

关于时间的话题是我在实际开发中遇到的一个问题。当要发布使用了JNI的程序时,并不一定要求客户要安装一个Java运行环境,因为可以在安装程序中打包这个运行环境。为了让打包程序利于下载,这个包要比较小,因此要去除JRE(Java运行环境)中一些不必要的文件。但是如果程序中用到Java中的日历类型,例如java.util.Calendar等,那么有个文件一定不能去掉,这个文件就是[JRE]\lib\tzmappings。它是一个时区映射文件,一旦没有该文件就会发现时间操作上经常出现与正确时间相差几个小时的情况。下面是打包JRE中必不可少的文件列表(以Windows环境为例),其中[JRE]为运行环境的目录,同时这些文件之间的相对路径不能变。

文件名 目录
hpi.dll [JRE]\bin
ioser12.dll [JRE]\bin
java.dll [JRE]\bin
net.dll [JRE]\bin
verify.dll [JRE]\bin
zip.dll [JRE]\bin
jvm.dll [JRE]\bin\classic
rt.jar [JRE]\lib
tzmappings [JRE]\lib

由于rt.jar有差不多10MB,但是其中有很大一部分文件并不需要,可以根据实际的应用情况进行删除。例如程序如果没有用到Java Swing,就可以把涉及到Swing的文件都删除后重新打包。

2.
Java Tip 17: Integrating Java with C++
Learn how to use C++ code from within a Java application and how to call from C++ to a Java object

http://www.javaworld.com/javaworld/javatips/jw-javatip17.html

Summary
This article explores some of the issues involved in integrating C++ code into a Java program. It shows how to call from a Java object to a C++ object, and how to call from a C++ object to a Java object. Interaction with garbage collection is explored, and a simple framework for integrating Java and C++ is developed. Finally, current solutions are compared with what can be expected from JDK 1.1, as well as from future offerings.

In this article, I'll discuss some of the issues involved in integrating C++ code with a Java application. After a word about why one would want to do this and what some of the hurdles are, I'll build up a working Java program that uses objects written in C++. Along the way, I'll discuss some of the implications of doing this (such as interaction with garbage collection), and I'll present a glimpse of what we can expect in this area in the future.

Why integrate C++ and Java?
Why would you want to integrate C++ code into a Java program in the first place? After all, the Java language was created, in part, to address some of the shortcomings of C++. Actually, there are several reasons why you might want to integrate C++ with Java:



Performance. Even if you're developing for a platform with a just-in-time (JIT) compiler, odds are that the code generated by the JIT runtime is significantly slower than the equivalent C++ code. As JIT technology improves, this should become less of a factor. (In fact, in the near future, good JIT technology may well mean that Java runs faster than the equivalent C++ code.)

For reuse of legacy code and integration into legacy systems.

To directly access hardware or do other low-level activities.

To leverage tools that are not yet available for Java (mature OODBMSes, ANTLR, and so on).

If you take the plunge and decide to integrate Java and C++, you do give up some of the important advantages of a Java-only application. Here are the downsides:



A mixed C++/Java application cannot run as an applet.

You give up pointer safety. Your C++ code is free to miscast objects, access a deleted object, or corrupt memory in any of the other ways that are so easy in C++.

Your code may not be portable.

Your built environment definitely won't be portable -- you'll have to figure out how to put C++ code in a shared library on all platforms of interest.

The APIs for integrating C and Java are works in progress and will very likely change with the move from JDK 1.0.2 to JDK 1.1.
As you can see, integrating Java and C++ is not for the faint of heart! However, if you wish to proceed, read on.

We'll start with a simple example showing how to call C++ methods from Java. We'll then extend this example to show how to support the observer pattern. The observer pattern, in addition to being one of the cornerstones of object-oriented programming, serves as a nice example of the more involved aspects of integrating C++ and Java code. We'll then build a small program to test our Java-wrapped C++ object, and we'll end with a discussion of future directions for Java.

Calling C++ from Java
What's so hard about integrating Java and C++, you ask? After all, SunSoft's Java Tutorial has a section on "Integrating Native Methods into Java Programs" (see Resources). As we'll see, this is adequate for calling C++ methods from Java, but it doesn't give us enough to call Java methods from C++. To do that, we'll need to do a little more work.

As an example, we'll take a simple C++ class that we'd like to use from within Java. We'll assume that this class already exists and that we're not allowed to change it. This class is called "C++::NumberList" (for clarity, I'll prefix all C++ class names with "C++::"). This class implements a simple list of numbers, with methods to add a number to the list, query the size of the list, and get an element from the list. We'll make a Java class whose job it is to represent the C++ class. This Java class, which we'll call NumberListProxy, will have the same three methods, but the implementation of these methods will be to call the C++ equivalents. This is pictured in the following object modeling technique (OMT) diagram:




A Java instance of NumberListProxy needs to hold onto a reference to the corresponding C++ instance of NumberList. This is easy enough, if slightly non-portable: If we're on a platform with 32-bit pointers, we can simply store this pointer in an int; if we're on a platform that uses 64-bit pointers (or we think we might be in the near future), we can store it in a long. The actual code for NumberListProxy is straightforward, if somewhat messy. It uses the mechanisms from the "Integrating Native Methods into Java Programs" section of SunSoft's Java Tutorial.

A first cut at the Java class looks like this:



public class NumberListProxy {
static {
System.loadLibrary("NumberList");
}
NumberListProxy() {
initCppSide();
}
public native void addNumber(int n);
public native int size();
public native int getNumber(int i);
private native void initCppSide();
private int numberListPtr_;
// NumberList*
}


The static section is run when the class is loaded. System.loadLibrary() loads the named shared library, which in our case contains the compiled version of C++::NumberList. Under Solaris, it will expect to find the shared library "libNumberList.so" somewhere in the $LD_LIBRARY_PATH. Shared library naming conventions may differ in other operating systems.

Most of the methods in this class are declared as "native." This means that we will provide a C function to implement them. To write the C functions, we run javah twice, first as "javah NumberListProxy," then as "javah -stubs NumberListProxy." This automatically generates some "glue" code needed for the Java runtime (which it puts in NumberListProxy.c) and generates declarations for the C functions that we are to implement (in NumberListProxy.h).

I chose to implement these functions in a file called NumberListProxyImpl.cc. It begins with some typical #include directives:



//
//
NumberListProxyImpl.cc
//
//
// This file contains the C++ code that implements the stubs generated
// by "javah -stubs NumberListProxy".
cf. NumberListProxy.c.
#include
#include "NumberListProxy.h"
#include "NumberList.h"


is part of the JDK, and includes a number of important system declarations. NumberListProxy.h was generated for us by javah, and includes declarations of the C functions we're about to write. NumberList.h contains the declaration of the C++ class NumberList.

In the NumberListProxy constructor, we call the native method initCppSide(). This method must find or create the C++ object we want to represent. For the purposes of this article, I'll just heap-allocate a new C++ object, although in general we might instead want to link our proxy to a C++ object that was created elsewhere. The implementation of our native method looks like this:



void NumberListProxy_initCppSide(struct HNumberListProxy *javaObj)
{
NumberList* list = new NumberList();
unhand(javaObj)->numberListPtr_ = (long) list;
}


As described in the Java Tutorial, we're passed a "handle" to the Java NumberListProxy object. Our method creates a new C++ object, then attaches it to the numberListPtr_ data member of the Java object.

Now on to the interesting methods. These methods recover a pointer to the C++ object (from the numberListPtr_ data member), then invoke the desired C++ function:



void NumberListProxy_addNumber(struct HNumberListProxy* javaObj,long v)
{
NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_;
list->addNumber(v);
}
long NumberListProxy_size(struct HNumberListProxy* javaObj)
{
NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_;
return list->size();
}
long NumberListProxy_getNumber(struct HNumberListProxy* javaObj, long i)
{ NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_;
return list->getNumber(i);
}


The function names (NumberListProxy_addNumber, and the rest) are determined for us by javah. For more information on this, the types of arguments sent to the function, the unhand() macro, and other details of Java's support for native C functions, please refer to the Java Tutorial.

While this "glue" is somewhat tedious to write, it's fairly straightforward and works well. But what happens when we want to call Java from C++?

Calling Java from C++
Before delving into how to call Java methods from C++, let me explain why this can be necessary. In the diagram I showed earlier, I didn't present the whole story of the C++ class. A more complete picture of the C++ class is shown below:




As you can see, we're dealing with an observable number list. This number list might be modified from many places (from NumberListProxy, or from any C++ object that has a reference to our C++::NumberList object). NumberListProxy is supposed to faithfully represent all of the behavior of C++::NumberList; this should include notifying Java observers when the number list changes. In other words, NumberListProxy needs to be a subclass of java.util.Observable, as pictured here:




It's easy enough to make NumberListProxy a subclass of java.util.Observable, but how does it get notified? Who will call setChanged() and notifyObservers() when C++::NumberList changes? To do this, we'll need a helper class on the C++ side. Luckily, this one helper class will work with any Java observable. This helper class needs to be a subclass of C++::Observer, so it can register with C++::NumberList. When the number list changes, our helper class' update() method will be called. The implementation of our update() method will be to call setChanged() and notifyObservers() on the Java proxy object. This is pictured in OMT:




Before going into the implementation of C++::JavaObservableProxy, let me mention some of the other changes.

NumberListProxy has a new data member: javaProxyPtr_. This is a pointer to the instance of C++JavaObservableProxy. We'll need this later when we discuss object destruction. The only other change to our existing code is a change to our C function NumberListProxy_initCppSide(). It now looks like this:



void NumberListProxy_initCppSide(struct HNumberListProxy *javaObj)
{
NumberList* list = new NumberList();
struct HObservable* observable = (struct HObservable*) javaObj;
JavaObservableProxy* proxy = new JavaObservableProxy(observable, list);
unhand(javaObj)->numberListPtr_ = (long) list;
unhand(javaObj)->javaProxyPtr_ = (long) proxy;
}


Note that we cast javaObj to a pointer to an HObservable. This is OK, because we know that NumberListProxy is a subclass of Observable. The only other change is that we now create a C++::JavaObservableProxy instance and maintain a reference to it. C++::JavaObservableProxy will be written so that it notifies any Java Observable when it detects an update, which is why we needed to cast HNumberListProxy* to HObservable*.

Given the background so far, it may seem that we just need to implement C++::JavaObservableProxy:update() such that it notifies a Java observable. That solution seems conceptually simple, but there is a snag: How do we hold onto a reference to a Java object from within a C++ object?

Maintaining a Java reference in a C++ object
It might seem like we could simply store a handle to a Java object within a C++ object. If this were so, we might code C++::JavaObservableProxy like this:



class JavaObservableProxy public Observer {
public:
JavaObservableProxy(struct HObservable* javaObj, Observable* obs) {
javaObj_ = javaObj;
observedOne_ = obs;
observedOne_->addObserver(this);
}
~JavaObservableProxy() {
observedOne_->deleteObserver(this);
}
void update() {
execute_java_dynamic_method(0, javaObj_, "setChanged",
"()V");
}
private:
struct HObservable* javaObj_;
Observable* observedOne_;
};


Unfortunately, the solution to our dilemma is not so simple. When Java passes you a handle to a Java object, the handle] will remain valid for the duration of the call. It will not necessarily remain valid if you store it on the heap and try to use it later. Why is this so? Because of Java's garbage collection.

First of all, we're trying to maintain a reference to a Java object, but how does the Java runtime know we're maintaining that reference? It doesn't. If no Java object has a reference to the object, the garbage collector might destroy it. In this case, our C++ object would have a dangling reference to an area of memory that used to contain a valid Java object but now might contain something quite different.

Even if we're confident that our Java object won't get garbage collected, we still can't trust a handle to a Java object after a time. The garbage collector might not remove the Java object, but it could very well move it to a different location in memory! The Java spec contains no guarantee against this occurrence. Sun's JDK 1.0.2 (at least under Solaris) won't move Java objects in this way, but there are no guarantees for other runtimes.

What we really need is a way of informing the garbage collector that we plan to maintain a reference to a Java object, and ask for some kind of "global reference" to the Java object that's guaranteed to remain valid. Sadly, JDK 1.0.2 has no such mechanism. (One will probably be available in JDK 1.1; see the end of this article for more information on future directions.) While we're waiting, we can kludge our way around this problem.

To safely keep references to Java objects, we can simply store the references in a Java vector. If we make this vector part of a singleton object that's globally available, the list will never get garbage collected, and we'll be able to get to the list from C++. Within a C++ object, the "reference" to the Java object can actually be an index into the singleton Vector. In my example, I've called the class that maintains this vector JavaObjectHolder. The structure of our entire system is shown below:




JavaObjectHolder is a straightforward Java class. It simply has methods to add an object (returning its index), remove an object, and get an object. For simplicity, I made these methods static. The declarations of the methods look like this:



class JavaObjectHolder {
public static int addObject(Object o) {
... }
public static void removeObject(int handle) {
...
}
public static Object getObject(int handle) {
...
}
}


(The complete source of this class, including exception specifications, can be found at the end of this article.)

Now that we have a way of maintaining references to Java objects, we're in a position to actually implement C++::JavaObservableProxy. The header is straightforward:



#if !defined(JavaObservableProxy_h)
#define JavaObservableProxy_h
#include "Observer.h"
#include "Observable.h"
class JavaObservableProxy : public Observer {
public:
JavaObservableProxy(struct HObservable* javaObj, Observable* obs);
~JavaObservableProxy();
void update();
private:
int javaObjectId_;
Observable* observedOne_;
};
#endif



Within the implementation file (JavaObservableProxy.cc), we first define a convenience function to get us the class descriptor of the JavaObjectHolder class:



static ClassClass* javaObjectHolder()
// Give a pointer to the class descriptor for JavaObjectHolder
{
static ClassClass* result = 0;
if (result == 0) {
result = FindClass(0, "JavaObjectHolder", TRUE);
assert(result != 0);
}
return result;
}


This is faster than calling the Java library function FindClass() each time.

Next, we need to write the constructor. The constructor simply calls JavaObjectHolder.addObject to convert the handle into an integer that's safe to store on the heap:



JavaObservableProxy::JavaObservableProxy(
struct HObservable* javaObj,
Observable* obs)
{
javaObjectId_ = execute_java_static_method(
0, javaObjectHolder(), "addObject",
"(Ljava/lang/Object;)I",
javaObj);
observedOne_ = obs;
observedOne_->addObserver(this);
}


When the C++::JavaObservableProxy is destroyed, we'll need to have the matching call to javaObjectHolder.removeObject()...



JavaObservableProxy::~JavaObservableProxy()
{
observedOne_->deleteObserver(this);
execute_java_static_method(
0, javaObjectHolder(), "removeObject",
"(I)V", javaObjectId_);
javaObjectId_ = -1;
}


Finally, we have the infrastructure we need. All that's left is to implement C++::JavaObservableProxy::update()...



void JavaObservableProxy::update()
{
HObject* javaObj = (HObject*)
execute_java_static_method(
0, javaObjectHolder(), "getObject",
"(I)Ljava/lang/Object;",
javaObjectId_);
// If an exception occurred, get back to the Java runtime, because
// invoking another method would clear the exception flag.
if (exceptionOccurred(EE()))
return;
execute_java_dynamic_method(0, javaObj, "setChanged",
"()V");
if (exceptionOccurred(EE()))
return;
execute_java_dynamic_method(0, javaObj, "notifyObservers",
"()V");
}


This method gets a handle to the Java proxy (by calling JavaObjectHolder.getObject()), then executes setChanged() and notifyObservers() on the Java object.

Garbage collection revisited
In this discussion, we've created a number of instances, but we haven't said anything about how to clean them up. Remember, this is C++, and we need to manually dispose of the objects we create!

An obvious place to dispose of the C++ objects we create would be within the finalize() method of NumberListProxy. Unfortunately, this won't work because there is a circular reference: NumberListProxy maintains a reference to C++::JavaObservableProxy, and C++::JavaObservableProxy maintains a reference to NumberListProxy (by going through the static Vector inside JavaObjectHolder). There is no way for Java's garbage collector to detect this circular reference, so NumberListProxy instance will never be collected.

To get around this, we must resort to manual memory management. We add a method called "detach()" to NumberListProxy. When the Java side is done with a NumberListProxy instance, it must call NumberListProxy.detach(). This method can free all of the C++ instances that are created. (Some languages support a concept called "weak references" that can solve problems of this nature in an automatic fashion. Weak references are not a part of Java, and a discussion of them would be beyond the scope of this article.)

Putting it all together
To demonstrate the system we've just developed, I created a simple application to exercise it. This application creates a number list, establishes an observer, and adds a few numbers to the list. Whenever a number is added, the observer is notified, and it prints a message to stdout. The Java observer is quite simple:



import java.util.*;
class NumberListObserver implements Observer {
NumberListObserver(NumberListProxy subject) {
subject_ = subject;
subject.addObserver(this);
}
/**
* Called when the subject changed
* @param o not used
* @param arg not used
**/
public void update(Observable o, Object arg) {
synchronized (subject_) {
// Don't want size() to change under us!
int sz = subject_.size();
System.out.print("
The list now has: ");
for (int i = 0; i < sz; i++) {
if (i > 0)
System.out.print(", ");
System.out.print(subject_.getNumber(i));
}
}
System.out.println("");
}
private NumberListProxy subject_;
// Thing being observed }



The main program looks like this:



import java.util.*;
class TestNumberList {
public static void main(String args[]) {
NumberListProxy model = new NumberListProxy();
NumberListObserver obs = new NumberListObserver(model);
System.out.println("Adding 3 to the list...");
model.addNumber(3);
System.out.println("Adding 42 to the list...");
model.addNumber(42);
System.out.println("Adding 666 to the list...");
model.addNumber(666);
System.out.println("Adding 7 to the list...");
model.addNumber(7);
model.deleteObserver(obs);
model.detach();
}
}


Running the program yields this output:



billf@pluto:~/javaC++Article/src$ java TestNumberList
Adding 3 to the list...
The list now has: 3
Adding 42 to the list...
The list now has: 3, 42
Adding 666 to the list...
The list now has: 3, 42, 666
Adding 7 to the list...
The list now has: 3, 42, 666, 7


JDK 1.1 and beyond
In their recent announcement, JavaSoft informed us that JDK 1.1 will have a "new Java native method interface" (see Resources). Hopefully, this new interface will provide a mechanism for getting a global reference to a Java object.

Netscape has comprehensive documentation on their JRI native method interface (see Resources). JRI ships with Netscape 3.0, and it provides everything we need in the way of registering global references. Unfortunately, it's only implemented by Netscape. Hopefully, JavaSoft will implement something similar, if not exactly the same. (Dare I hope that the JDK 1.1 will have something along the lines of Netscape's JRI?)

ILOG has announced a project called TwinPeaks (being developed with JavaSoft -- see Resources), which promises to "deliver Internet-ready C++ business application components to developers and customers." I expect this means that it will automate the writing of some of the glue code that we wrote by hand in this article. It will probably include other useful tools -- perhaps a debugger that can step from Java into C++.

Getting the source code
If you'd like the complete source code for the program developed in this article (complete with Solaris Makefile), click for javaAndC++.tar.gz, or for javacpp.zip. If you'd just like to browse, here are the source files:



Java source files:


JavaObjectHolder.java

NumberListObserver.java

NumberListProxy.java

TestNumberList.java

C++ source file:


JavaObservableProxy.h

JavaObservableProxy.cc


NumberList.h

NumberList.cc


NumberListProxyImpl.cc


Observable.h


Observable.cc


Observer.h


Other


Makefile (for Solaris-based
systems)


Conclusions
Integrating C++ classes into a Java application is fairly straightforward, although somewhat messy. We expect the standard native method APIs to improve in the near future. Once this happens, the general approach outlined in this article will continue to work, but the implementation will be easier (and performance will be better).

Accessing C++ from Java can be worth the inconvenience. It opens up large bodies of "legacy" C++ code to potential reuse within Java applications. It also lets us exploit the power of C++, where C++ has an advantage -- that is, in performance (at least for now), in directly accessing hardware, in doing other low-level activities, and so on. With this power comes danger, however. We lose the pointer safety that Java provides us, opening ourselves up to memory-corruption bugs. In other words, integrating C++ with Java can be a powerful technique but one to be used with care!

Friday, May 28, 2004

 

Some notes on Skin

1.
关于如何换肤、子类化的解决方案的讨论

The author is unknown

对于应用程序的换肤及子类化。下面是我尝试过一些方法,以在CAboutDlg中子类化其中的Button为例:
第一种:直接用现成的类
1.自己写一个类class CButtonXP : public CButton{/*...*/}
用MessageMap处理感兴趣的消息。
2.用CButtonXP代替CButton来声明变量m_btn;

3.在void CAboutDlg:oDataExchange(CDataExchange* pDX)中加上一句:
DDX_Control(pDX, IDB_BUTTON1, m_edit);
或者在InitDialog()中加上
m_btn.SubclassDlgItem(IDB_BUTTON1, this);
这两种效果差不多的。

第二种:在Hook中使用现成的类。
1.自己写一个类class CButtonXP : public CButton{/*...*/}
用MessageMap处理感兴趣的消息。
2,用g_hWndProcHook = ::SetWindowsHookEx(WH_CALLWNDPROC,
WndProcHook, NULL, ::GetCurrentThreadId());安装一个钩子
3. 在WndProcHook中处理窗口创建和销毁的消息
LRESULT CALLBACK WndProcHook(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION)
{
switch (((CWPSTRUCT*) lParam)->message)
{
case WM_CREATE:
BeginSubclassing(((CWPSTRUCT*) lParam)->hwnd);
break;

case WM_NCDESTROY:
// TODO: clear subclass info.
EndSubclassing(((CWPSTRUCT*) lParam)->hwnd);
break;

default:
break;
}
}

return CallNextHookEx(g_hWndProcHook, code, wParam, lParam);
}
4. 在BeginSubclassing中用GetClassName得到类名,例如"Button",然后用CButtonXP类进行子类化.
CButtonXP pButton = new CButtonXP
VERIFY(pButton ->SubclassWindow(hWnd));

第三种 在Hook中使用窗口过程。
1. 自己写一个按钮的窗口过程
WNDPROC oldProc;
LRESULT CALLBACK ProcButton(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
ASSERT(oldProc != 0);
if (oldProc == 0) return TRUE;
switch (uMsg)
{
case WM_ERASEBKGND:
break;
//......
default:
break;
}

return CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam);
}
2.同第二种
3.同第二种
4.在BeginSubclassing中得到类名后,用SetWindowLong的方式子类化
oldProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC);
SetWindowLong(hWnd, GWL_WNDPROC, (LONG) ProcButton);

第四种:不用Hook
在一个对话框的OnInitDialog中枚举它的所有子窗体
例如用下面两句来实现
hWnd=GetWindow(hDlg,GW_CHILD);
hWnd=GetWindow(hWnd,GW_HWNDNEXT);
对每个子窗体进行子类化处理,处理过程同第二种与第三种。

第五种:如果是在XP下运行,可以使用manifest,也就是如下的一个XML文件


name="Microsoft.Windows.XXXX"
processorArchitecture="x86"
version="5.1.0.0"
type="win32"/>
Windows Shell


type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="x86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>



把它存为 应用程序名.manifest,放到和应用程序对应的目录下
或者把它作为资源类型为24的资源编译进应用程序中。
这样程序在XP下就自动拥有了XP的风格。

第六种:使用第三方的库Skin++(www.uipower.com)实现换肤。
第七种:用第三方应用程序给整个windows换肤(windowblinds)。

以上七种方式各有优缺点。我在使用过程中也遇到不少问题,现在一一道来,希望和大家共同解决问题。
先排除几种不准备深入探讨的方式:
第五种 manifest方式 最快速和简洁,但是功能有限,存在严重的平台限制,
不过好处在于应用程序可以和windows共一种风格。
第六种 使用第三方的库Skin++(www.uipower.com)实现换肤方式 使用起来很简单,定制性也不错, 可供选择的皮肤种类非常的多, 支持的语言非常广泛,可以称得上是换肤功能的终结者,对于共享软件开发者和 注重界面的企业来说是个不错的解决方案,他的换肤理念很新,有些地方做得很独特,比如可以对BCG换肤等,有些技术点,很多同类产品都没有做到,比如ComboBox的滚动条,系统对话框(open or close Dialog)的菜单等等。
第七种, 属于自娱性质的,也就不多说了。
第一种, 直接使用现成的类,属于很常见的一种用法,一般来说使用上不会出什么问题,缺点就不说了,
如果这种方式让我满意,我就不必发这篇帖子了。

下面看看第二三四种:
第二种是用HOOK+窗口类,实现起来比较方便,和做一个自绘控件的工作量其实是一样的。
第三种是用HOOK+窗口过程,实现起来比较麻烦,需要自己处理一堆switch case, 自己转换消息参数,
自己找地方维护一堆状态变量,工作量很大。
第四种不用HOOK的方式,有个缺点:对被换肤的程序的源代码的修改比较多。当然,直接到进程中去找窗口句柄,
然后子类化那么就不用源代码了,不过这样的话还不如用HOOK呢。
实际上,HOOK机制和枚举窗体虽然过程不同,不过最终目的是一样的,都是为了子类化窗口。所以在此不去探讨孰优孰劣了。

现在切入正题,谈谈在子类化过程中遇到的问题:
重复subclass的问题
上面提到,子类化的两种方式:用窗口类或者用窗口过程。
使用窗口类是从CWnd派生一个类,调用CWnd的protected函数SubclassWindow.
可是如果正常使用一个窗口类(声明成员变量,加入DDX_Control),实际上在DDX_Control中也是是用了SubclassWindow的。
假如为一个控件声明变量,而在Hook中又进行了子类化,结果会怎么样呢?
答案是,程序崩溃或弹出消息框"不支持的操作"。
因为SubclassWindow函数调用前是要先Attach到一个HWND上去的。重复的Attach看来是不允许。
要避免程序崩溃也有办法:
1. 只为控件声明一个指针变量,动态的去获取CWnd类的实例,但是这样就达不到换肤的目的了。
2. 还有一种方法,经过我试验,如果两个SubclassWindow的调用位于不同的模块,例如一个位于exe,一个位于dll(我是通过exe中调用dll中的函数显示该dll中的对话框来测试的),那么就不会出现问题。在还没有找到更好的方法之前,这也姑且算是一种解决方法吧。

但是如果使用窗口过程来子类化,就不存在重复subclass的问题了,只要小心处理,子类化无数次都没问题,但是对于复杂的自绘事件,在一个窗口过程中来写switch语句,好像很麻烦。
我尝试过自己写一个新的SubclassWindow函数来尝试借用CWnd的窗口过程,这样就可以按照MFC的方式来写消息响应函数了。只可惜,最终还是无功而返,因为SubclassWindow不是虚函数,而CWnd的窗口过程是作为一个protected成员存在的。所以没法在外部借用MFC的消息机制。
所以,自己写代码处理wParam和lParam看来在所难免。

子类化系统对话框的问题。
系统的对话框和自己的对话框表现的总不一样。
目前我还没有对所有的系统对话框进行测试。在MessageBox弹出的对话框中遇到的问题可以见我这一片帖子:
http://community.csdn.net/Expert/To....asp?id=3103399
在文件对话框中我遇到一个问题,子类化过的CStatic的背景好像没有重绘一样,照理说应该由CStatic的父窗体负责背景的。
我在我的CStaticNew类中只重载了OnPaint,里面只处理文字和图标的绘制,背景的绘制留给父窗体完成。这样的处理在MessageBox和自己的AboutDlg中都没有问题,Static控件的背景就是父窗口的背景,可是在CFileDlg中背景就没有重绘了
void CStaticNew::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: Add your message handler code here
CRect rt;
GetWindowRect(rt);

// 绘制背景

dc.SetBkMode(TRANSPARENT);
// 绘制文字
CFont *pfont, * pOldFont;
pfont = GetFont();
if (pfont)
pOldFont = dc.SelectObject(pfont);

CString szTitle;
GetWindowText(szTitle);
dc.DrawText(szTitle, CRect(0, 0, rt.Width(), rt.Height()), DT_LEFT | DT_WORDBREAK );

if (pfont)
dc.SelectObject(pOldFont);
// 绘制图标
if ((GetStyle() & SS_ICON) != 0)
{
dc.DrawIcon(0, 0, GetIcon());
}
// Do not call CStatic::OnPaint() for painting messages
}

类名的识别问题
到现在为止,我所使用的子类化方法都是基于GetClassName这个函数获得窗口类名,再根据用spy++所得到的知识,
如"#32770"表示对话框,"ToolbarWindow32"是工具栏,等等。但是窗口类名是可以在创建时任意指定的呀,
而像CMainFrame的类名根本就不能够确定,例如记事本主窗体的类名是"Notepad",写字板主窗体的类名是"WordPadClass"
这样的话,子类化如何去进行呢。真想知道windows是怎么做的,skinmagic又是怎么做的。

目前主要就是这三个问题了。
希望大家能展开讨论,给出一个换肤的完善的解决方案

我写了一个简化的CWnd类来解决重复子类化问题和简化窗口过程,不过它不支持对自己的重复子类化(即只能用于没有被子类化的或者被CWnd子类化的HWND)。
因为不想弄得和MessageMap那样复杂,所以功能也有限:须手工转化WPARAM和LPARAM、消息处理无法继承、不支持多线程。

使用很简单,CWndNew* pWnd = new CWndNew;
pWnd->SubclassWindow(hWnd);

用完了,记得pWnd->UnsubclassWindow();和delete pWnd;
如果要进行功能扩充(继承),就改写那几个虚函数

class CWndNew
{
public:
CWndNew();
virtual ~CWndNew();
bool SubclassWindow(HWND hWnd);
void UnsubclassWindow();
protected: // virtual
virtual LRESULT WindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam);
virtual void PresubclassWindow(){};
virtual void PostunsubclassWindow(){};

protected:
LRESULT PrevWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam);
HWND m_hWnd;
private:
WNDPROC m_oldProc;
static map m_map;
static LRESULT CALLBACK StaticWindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
};

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
map CWndNew::m_map;
CWndNew::CWndNew()
{
m_hWnd = NULL;
}

CWndNew::~CWndNew()
{
ASSERT(m_hWnd == NULL);
}
bool CWndNew::SubclassWindow(HWND hWnd)
{
m_map[hWnd] = this;
ASSERT(m_hWnd == NULL);
m_hWnd = hWnd;
//允许派生类在子类化之前做一些初始化.
PresubclassWindow();

m_oldProc = (WNDPROC) GetWindowLong(hWnd, GWL_WNDPROC);
ASSERT(m_oldProc != 0);
SetWindowLong(hWnd, GWL_WNDPROC, (LONG) StaticWindowProc);
return true;
}

void CWndNew::UnsubclassWindow()
{
SetWindowLong(m_hWnd, GWL_WNDPROC, (LONG)m_oldProc);
PostunsubclassWindow();
m_map.erase(m_hWnd);
m_hWnd = NULL;
}

LRESULT CALLBACK CWndNew::StaticWindowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
CWndNew* pWnd = m_map[hWnd];
ASSERT(pWnd != NULL);
return pWnd->WindowProc(uMsg, wParam, lParam);
}

LRESULT CWndNew::PrevWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam);
}

LRESULT CWndNew::WindowProc(
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
return PrevWindowProc(uMsg, wParam, lParam);
}
关于子类化及其撤销的顺序问题:

当用自己的类或者过程子类化窗口时,需要处理好与MFC类子类化的顺序冲突。
假设我们自己的类叫CWndNew,那么不管CWnd和CWndNew谁先子类化一个窗口,最终两者

协同工作的结果应该是该窗口的窗口过程还原到未子类化之前的状态。
首先,不要在HOOK过程中处理WM_NCDESTROY消息。
理由:如果CWndNew比CWnd先子类化,由于HOOK的原因,你仍然会先处理WM_NCDESTROY

,这时候如果你撤销子类化,那么CWnd类就得不到机会清理。而如果你不撤销子类化

,CWnd没有能力把被子类化的窗口还原到最初状态。在HOOK过程中,不能通过调用

SendMessage函数让CWnd先行处理,然后你自己再处理,因为SendMessage后,消息又会被HOOK拦截。

由于上述原因,在CWndNew的消息过程中处理WM_NCDESTROY是很不错的选择,MFC也是这样做的。
参照如下的代码进行解释:
case WM_NCDESTROY:
{
LRESULT lret;
WNDPROC wndproc;
wndproc = (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC);
if (wndproc == CWndNew::StaticWindowProc)
{
HWND hWnd = m_hWnd;
UnsubclassWindow();
lret = CallWindowProc(m_oldProc, hWnd, uMsg, wParam, lParam);
}
else
{
lret = CallWindowProc(m_oldProc, m_hWnd, uMsg, wParam, lParam);
if(wndproc == (WNDPROC)GetWindowLong(m_hWnd, GWL_WNDPROC))
UnsubclassWindow();
}
delete this;
return lret;
}
首先判断该窗口的WNDPROC是否发生过变动,如果没有的话是最好的,赶紧撤销子类化,再把消息传递给之前窗口过程,然后功成身退,不问世事了。
如果发生过变动,那么也就是说有别的类在CWndNew子类化以后又进行了子类化,而现在又把WM_NCDESTROY传给了CWndNew。这好办,如法炮制,把消息继续往前传,如果WNDPROC又发生了改变,说明之前的某个窗口过程已经作了处理,就不需要再进行撤销子类化的操作了。这点MFC的CWnd类也是这样做的。


另外还有一个问题不解:
就是Edit,ListBox,ListCtrl等等控件的内嵌的滚动条是怎么换肤的?
网上一般介绍的方法是隐藏原来的,然后换上自己重新实现的。
这种在Spy++中一看就能现出原形,可是Skin++ 换肤后的滚动条就不知道是怎么实现的了,不知有谁知道?

2.
SkinControls 1.1 - A journey in automating the skinning of Windows controls
By .dan.g.

A self-contained, user-extensible, application-wide skinning architecture for Windows controls
http://www.codeproject.com/useritems/SkinCtrl.asp

3.
Effecto Player
By Ahmed Ismaiel Zakaria

Media audio player with 3D and 2D effects and skinning.
http://www.codeproject.com/audio/Effecto.asp

4.
作者:易剑

为了使每个窗体的标题栏都能定制,并且不用为每一个窗体类编码,所以本方法采用钩子技术,其核心思想是监控 Windows 消息,处理需要重缓标题的消息,以达到定制标题栏的思想.
本文件介绍的方法将在应用程序中安装 WH_CALLWNDPROC 钩子,具体的代码如下所示:

在应用程序启动时安装钩子的代码:
extern "C" BOOL __declspec(dllexport) InstallCallWndHook()
{
g_hCallWndProc = SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, AfxGetInstanceHandle(), GetCurrentThreadId());
if (NULL == g_hCallWndProc)
return FALSE;
else
return TRUE;
}

InstallCallWndHook() 函数定义为出口函数,在需要定制标题的程序中将调用它,由于安装的是 WH_CALLWNDPROC 钩子,所以在应用程序调用自己的窗体过程之前,总会先调用 CallWndProc;如果设置为 WH_CALLWNDPROCRET 则顺序刚好相反
g_hCallWndProc为一内存共享变量,其它定义的方法如下所示:

#pragma data_seg("Shared")
static HHOOK g_hCallWndProc;
#pragma data_seg()

当然你也可以定义为其它形式,比如直接采用共享内存API创建方式.

在应用程序退出时安装卸载钩子的代码:
extern "C" void __declspec(dllexport) UnInstallCallWndHook()
{
if (g_hCallWndProc != NULL)
{
UnhookWindowsHookEx(g_hCallWndProc);
g_hCallWndProc = NULL;
}
}

定制标题栏的入口函数为 CallWndProc(),其代码如下:
LRESULT __declspec(dllexport) CALLBACK CallWndProc(
int code, // hook code
WPARAM wParam, // undefined
LPARAM lParam // address of structure with message data (CWPSTRUCT)
)
{
DWORD dwThreadID = (DWORD)wParam;
LPCWPSTRUCT pCwpStruct = LPCWPSTRUCT(lParam);
if (HC_ACTION == code)
{
if ((pCwpStruct->message == WM_MOUSEMOVE)
|| (pCwpStruct->message == WM_SETCURSOR)
|| (pCwpStruct->message == WM_NCHITTEST)
|| (pCwpStruct->message == WM_KICKIDLE)
|| (pCwpStruct->message == WM_NCMOUSEMOVE)
|| (pCwpStruct->message == WM_MOUSEACTIVATE)
|| (pCwpStruct->message > WM_USER))
;
else
DrawFrame(pCwpStruct);
}

return CallNextHookEx(g_hCallWndProc, code, wParam, lParam);
}

上面代码中的 if 语句主要用来判断收到哪些消息时需要重绘标题栏,有兴趣的朋友可以对这段代码进行改进

在函数 DrawFrame 中将实现对窗体标题栏和边框的绘制,标题的绘制有两种方法,一是直接画图,二是贴图的方式。在本文中将实现两种方法,如果在当前目录下有 active.bmp 和 inactive.bmp 两个文件,则采用它们所代表的位图作为窗体的标题栏,否则采用画图的方式

由于只绘制标题栏,所以需要对 CallWndProc 进行过滤,对于非窗体如 Button 则不进行绘制,本文中仅以如下简单的方法来处理:
char szClassName[128] = {0};
::GetClassName(pCwpStruct->hwnd, szClassName, sizeof(szClassName));
if (strcmp(szClassName, "#32770") != 0)
return ;

实际中不能这样中,因为很多窗体的类名可能不是"#32770",比较好的方法建议去判断 pCwpStruct->hwnd 所代表的对象是否有父窗体,调用 GetParent 判断一下即可

在正式绘制之前还必须判断窗体是处于活动状态还是非活动状态,这样就可以区分在两种不同的状态下绘制不同的标题栏和边框了。

下面这段代码就是用来绘制标题栏的:

if (bActive)
hBitmap = (HBITMAP)::LoadImage(NULL, _T("active.bmp"), IMAGE_BITMAP, nWidth, nHeight, LR_LOADFROMFILE);
else
hBitmap = (HBITMAP)::LoadImage(NULL, _T("inactive.bmp"), IMAGE_BITMAP, nWidth, nHeight, LR_LOADFROMFILE);

if (NULL == hBitmap)
{
DrawTitleBar(dcWin, rcNcClient, 0);
}
else
{
dcMem = ::CreateCompatibleDC(dcWin);
hOldBitmap = (HBITMAP)::SelectObject(dcMem, hBitmap);

::StretchBlt(dcWin,
0,0,
nWidth, nHeight,
dcMem,
0,0,
nWidth, nHeight,
SRCCOPY);

::SelectObject(dcMem, hOldBitmap);
::DeleteDC(dcMem);
}


其中变量bActive为TRUE时表示窗体处于活动状态,为FALSE时表示窗体处于非活动状态。两个 LoadImage 函数分别用来将两种状态下的位图装载到内存中,以便下一步进行贴图.当 LoadImage 不成功时,表示当前目录下没有 active.bmp 和 inactive.bmp 文件中或文件格式不正确,在这处情况下就调用 DrawTitleBar 函数对标题栏进行绘画。绘画的方法可以随便,但要绘在矩形 rcNcClient 内,因为这个矩形就是标题栏所在区域。
如果 LoadImage 成功,则直接将位图贴到标题栏中。接下来就是绘制边框了,在绘制之前还需要计算出边框的所在矩形,然后再在dcWin上按要求进行绘制即可。

这种方法的关键地方是安装合适的钩子,然后对合适的消息进行处理,采用这种方法可以改变几乎任何一可见窗体的外观,包括其它程序的窗体等,对于特殊的窗体等只需要进行专门处理即可

由于编译成的是 DLL 文件中,所以可以很轻松的运用到其它程序中。只需要在需要用到的程序中调用 InstallCallWndHook 安装这个钩子即可。

5.
讨论有关XP界面的制作

by 韩举,北斗龙,china7445,黄翼等

1、如何让window9x\win2000下的标题栏增高?(能正确保证菜单、工具栏依次下将同样高度。)

设置标题栏高度,必须重载OnNcCalcSize(WM_NCCALCSIZE消息)察看MSDN可以得到下面结构
NCCALCSIZE_PARAMS *lpncsp
设置lpncsp->rgrc[0].top和lpncsp->rgrc[0].bottom就可以改变高度

2、怎样保证标题栏不露形(尤其是按钮那一块,鼠标移到非客户区时,默认按钮全部露出来了,难道要在每次WM_NCMOUSEMOVE后都重画一次标题栏?太消耗资源了吧)

不露形的原因是不显示默认按钮,不显示当然不会露出来了,
我是在CHJSkinWindow::OnInitWindow中实现的
DWORD style = GetWindowLong( m_hWnd, GWL_STYLE );

this->bsizable=style & WS_SIZEBOX; //保持状态
this->bminable=style & WS_MINIMIZEBOX; //保持状态
this->bmaxable=style & WS_MAXIMIZEBOX; //保持状态

style &= ~WS_MINIMIZEBOX; //去掉默认按钮
style &= ~WS_MAXIMIZEBOX; //去掉默认按钮
style &= ~WS_SYSMENU; //去掉默认按钮
SetWindowLong( m_hWnd, GWL_STYLE, style );

在画非客户区时必须重载WM_NCPAINT消息,在他中画,默认的去掉了,当然可以随便画了。
重画时可以设置无效区域
CDC::GetClipBox函数可以取得无效的区域,这样可以提高速度。

However,处理WM_NCCALCSIZE消息只是增加非客户区高度,对非客户区的里的菜单等位置并无改变。对于改变窗体的style,尤其是那个WS_SYSMENU,将使得你的程序在任务栏上无图标。



But,菜单栏的问题OFFICE中早就解决了,用ToolBar等模仿菜单。



对于非客户区域的计算和绘制,如果在处理WM_NCPAINT的过程中调用了DefWindowProc(),那么Windows将使用自己的解决方案:
对于非客户区域中的各种元素(窗体边框,比标题条,菜单条,滚动条)Windows都只使用内部已定义好的对应的尺寸常量
SM_CXFRAME, SM_CYFRAME,SM_CXDLGFRAME, SM_CYDLGFRAME,SM_CXBORDER,SM_CYBORDER,SM_CYMENU,SM_CXVSCROLL,SM_CYHSCROLL
这些常量通过GetSystemMetrics()获得,程序员不能通过编程改变(但是可以通过显示属性对话框中的外观选项进行设置--这是对整个系统设置而不是对某个进程)

下面试给一段窗体非客户区域的代码

void NCPaint(HWND hwnd)
{
DWORD dwStyle = GetWindowStyle();
if (dwStyle & WS_MINIMIZE || !IsWindowVisible(hwnd))
return;

// 获得HDC
HDC hdc = GetWindowDC(hwnd);

// 非客户区域
CRect rect; GetWindowRect(&rect);
rect.top = rect.left = 0; // 初始化为0
...

// 1 画边框
if (style & WS_THICKFRAME)
{
NCDrawFrame(hdc, &rect);
}
else if (style & WS_BORDER)
...

// 修改位置
InflateRect(rect, GetSystemMetrics(SM_CX...), GetSystemMetrics(SM_CY...));

// 2 画标题条
if (wStyle & WS_CAPTION)
{
NCDrawCaption(...);

// 修改位置
rect.top += GetSystemMetrics(SM_CYSIZE) + GetSystemMetrics(SM_CYBORDER);
}

// 3. 画菜单
if (有菜单)
{
NCDrawMenuBar(...);

// 修改位置
rect.top += GetSystemMetrics(SM_CYMENU);
}

// 4. 画滚动条
if (dwStyle & WS_VSCROLL)
DrawScrollBar();
if (dwStyle & WS_HSCROLL)
DrawScrollBar();

// 5. 画"size-box"
}

可见对于Windows缺省的布局程序员是能控制的,除非你不调用DefWindowProc()。但要做的工作远不止WM_NCPAINT,还用很多别的消息如WM_NCACTIVATE等。

hupflv 同志认为使用全局钩子会降低系统性能,这是对的,但是除非你不使用自画菜单
那么就很难避免(因为必须捕获下拉菜单的创建),事实上大部份都是使用窗体子类化技术
(替换窗体处理函数),系统不会太多的开销,尤其适用ATL引入的Thunk技术就没有多余的开销



将对话框资源设为无标题,编译执行后,任务栏上无图标;
但你可以通过一点处理后,可以使它有图栏,并且有最大化,最小化,大小,移动等菜单项可用。

int style=::GetWindowLong( this->GetSafeHwnd(), GWL_STYLE );
::SetWindowLong(this->GetSafeHwnd(), GWL_STYLE, style|WS_SYSMENU|WS_MAXIMIZEBOX|WS_MINIMIZEBOX);

要“大小”菜单项可用,只需将窗体的Border设为Resizing即可。因为没有标题栏,所以无从谈到默认的露型。



非客户区增加,用的便是WM_NCCALCSIZE
传入的第一个参数为FALSE时,第二个参数便为重新设置的客户区区域(通过对客户区下移,便让非客户区增加了)为TRUE的话,则需要传入一个NCCALCSIZE_PARAMS结构,一般是窗体创建时才用的。



用资源模板跟后来加风格确实有一点区别,那主要是MFC中窗口类加载资源时作了很多其它设置,而我们加风格时,只是很简单的动作。

所以要想去掉那些不良特性(闪烁,露形),那你就得动态去掉标题栏,然后在客户区开一块非客户区,模拟标题栏。但这时候,你要做其它较多事情,如对标题上各类响应。


如果拖动对话框的边框(sizing)时边框会不停的闪烁(我的系统本身是xp)。原因在于原始代码:
case WM_NCPAINT:
lReturn=oldProc(hWnd, uMsg, wParam, lParam); // 这个语句调用了系统缺省处理,画出了系统边框
s_pThis->DrawDialog(hWnd);// 再画一次覆盖了原来的边框,所以出现了闪烁
return lReturn;

我一开始想到的解决办法是不要系统缺省处理,直接画边框然后return一个值,后来trace发现这个值是变化的而且很有用,直接return一个固定的值会导致程序错误,没办法只好继续调用缺省处理。

为了解决闪烁,后来找到一个在标题栏自绘按钮的例子,发现上面有个解决办法可以解决这个问题,就是调用系统缺省处理前先使窗口不可见,调用完之后再恢复窗口可见。如下:

建议修改:
case WM_NCPAINT:
dwStyle = GetWindowLong(hWnd, GWL_STYLE);// turn off WS_VISIBLE
SetWindowLong(hWnd, GWL_STYLE, dwStyle & ~WS_VISIBLE);
lReturn=oldProc(hWnd, uMsg, wParam, lParam);// turn on WS_VISIBLE
SetWindowLong(hWnd, GWL_STYLE, dwStyle);
s_pThis->DrawDialog(hWnd);
return lReturn;

Thursday, May 27, 2004

 

Some notes on Taskbar

1.
(按:今天有朋友问起这个问题,恰又发现以前的一包资料,遂有此篇转载。剩下的资料正好填以前blog的坑)

作者:sunyuzhe114
出处:http://www.csdn.net

[quote]
为了说明这个问题,我测试了下面的软件,我的机器是p3-450,192mb,win2000:
天网防火墙2.4.8,
腾讯QQ简体标准版2000C Build 0305b
简体测试版QQ2000C Build 0510
金山公司的金山词霸2002,
foxmail 4.1

当上述程序运行后,它们会在屏幕的右下脚的托盘中增加一个小图标,当我们打开进程管理器时,把系统的explore.exe结束,这时windows下的任务栏和开始菜单就消失了,这是我们再能过进程管理器运行ie(我的是在D:\WINNT\explorer.exe),这时windows下的任务栏和开始菜单就又出现了,但是我们注意到原来的一些程序在屏幕的右下脚的托盘中增加一个小图标不见了,但是该程序的进程仍在进程管理器中可见,这是问题就出现了,我们的程序就无法在用户的控制下运行了。经测试上述软件除foxmail 4.1存活下回去,其它无一幸免。同时我还测试了一下国外的一软件,如诺顿,winmap等,做同样的测试,它们的程序图标在ie恢复后仍可出现在任务栏中。而且存在bug的程序远不止这些程序,只要是在屏幕的右下脚的托盘中增加一个小图标的程序80%都存在这个问题。我写这篇文章的目的就是要告诉大家,这个bug很容易修复的,以一个基于对话框的vc++6.0程序为例,
首行应在对话框类中加入如下变量。
NOTIFYICONDATA m_tnid;
再定义一个全局变量
#define MYWM_NOTIFYICON (WM_USER+100)

我们知道,写一个托盘程序就要在OnInitDialog()中加入如下代码就可以了
m_tnid.cbSize=sizeof(NOTIFYICONDATA);
m_tnid.hWnd=AfxGetMainWnd()->m_hWnd;
m_tnid.uFlags=NIF_MESSAGE|NIF_ICON|NIF_TIP;
m_tnid.uCallbackMessage=MYWM_NOTIFYICON;
strcpy(m_tnid.szTip,"Only 1.99 $");
m_tnid.uID=IDR_MAINFRAME;
HICON hIcon;
hIcon=AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_tnid.hIcon=hIcon;
::Shell_NotifyIcon(NIM_ADD,&m_tnid);
if(hIcon)::DestroyIcon(hIcon);
这时运行程序,就会在系统托盘中加入一个我们的图标, 但是我们退出程序时那个右下
脚的图标并不会消失,只有把鼠标移到图标那里图标才会消失。我们可以重载程序的
关闭窗口函数,在OnCancel() 函数中加入
NOTIFYICONDATA tnid;
tnid.cbSize=sizeof(NOTIFYICONDATA);
tnid.hWnd=AfxGetMainWnd()->m_hWnd;
tnid.uID=IDR_MAINFRAME;//保证删除的是我们的图标
Shell_NotifyIcon(NIM_DELETE,&tnid);
这时我们退出程序时,屏幕的右下脚的托盘中增加一个小图标也会随之消失。也许许多人包括我在内,都会觉得我样的程序就很完美了,但是当有时ie执行了非法操作被关闭时,我们重新恢复ie时刚才写过的程序的光标在任务栏中不见了。也许你会说我们的程序在任务栏中还有最小化的窗口,的确我们写是程序是这样的,但是我们测试的程序在最小化以后在任务栏中是没有窗口的如果是这样,当它们的小图标在屏幕的右下脚的托盘中消失后,一般情况下你是无法让它们再出来了。当然你是高手你可以自己写个程序,找到那个消失光标的程序的句柄,用一个showwindow让它出来,但是通常你不得不再用进程管理器重新结束那此程序的进程。这就是上述国产软件的通病。也许这样并不会造成什么损失,不过有一次我用sterm 1.0 bet时挂在上面2天,结果因为ie掉死了所以2天的48小时的时间就没有加到我的经验值里。不过现在的sterm 就没有这个bug了。那么我们该怎么解决这个问题呢。其实很简单。
在csdn的讨论中,经过 jiangsheng(蒋晟卧病中)的帮助,我得出结论
我们只要在全局const UINT WM_TASKBARCREATED =
::RegisterWindowMessage(_T("TaskbarCreated"));//这个消息是系统开始菜单,任务栏创建时发出的
再加上消息映射表
ON_REGISTERED_MESSAGE(WM_TASKBARCREATED,
OnTaskBarCreated)
OnTaskBarCreated()函数如下:
afx_msg void OnTaskBarCreated();

void CTianWangDlg::OnTaskBarCreated()
{
m_tnid.cbSize=sizeof(NOTIFYICONDATA);
m_tnid.hWnd=AfxGetMainWnd()->m_hWnd;
m_tnid.uFlags=NIF_MESSAGE|NIF_ICON|NIF_TIP;
m_tnid.uCallbackMessage=MYWM_NOTIFYICON;
strcpy(m_tnid.szTip,"Only 1.99 $");
m_tnid.uID=IDR_MAINFRAME;
HICON hIcon;
hIcon=AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_tnid.hIcon=hIcon;
::Shell_NotifyIcon(NIM_ADD,&m_tnid);
}

这时现运行我们的程序,重复开始的过程,当ie恢复时我们的图标又会在系统托盘中出现了。
[/quote]

我正式出的程序都没有这个问题。

2.
Taskbar Sorter Utility
By Paul S. Vickery
http://www.codeproject.com/tools/tbarsort.asp

Utility to change order of icons in taskbar

3.
Taskbar Notification dialog
By John O'Byrne
http://www.codeproject.com/dialog/TaskbarNotifier.asp

A MSN IM-style popup notification dialog

4.
CTrayIconPosition - where is my tray icon?
By Irek Zielinski
http://www.codeproject.com/shell/ctrayiconposition.asp

Ever wanted to know position of your tray icon? Windows supplies no API for that. This class is a compact solution that works.

5.
作者:kingcom_xu
出处:http://www.csdn.net/develop/article/17/17482.shtm

VB的ShowInTaskbar功能分析以及用VC的实现 kingcom_xu(原作)

关键字 ITaskbarList,任务栏按钮,WS_EX_APPWINDOW,WS_EX_TOOLWINDOW



  在VB中想要显示或隐藏一个窗口在任务栏上的按钮很容易,直接设定一个form的ShowInTabkbar属性即可。但在SDK中却不是一件易事,一个窗口在什么情况下会在任务栏上显示一个按钮,在什么情况下不显示呢?这是有规定的:

0,无论什么情况,要让一个窗口在任务栏上显示按钮的前提是该窗口是可见的.

1.如果一个窗口是顶级窗口(也就是父窗口为NULL,也就是父窗口为桌面窗口),那么Windows将为它在任务栏上创建一个按钮.(有例外,见3)

2.如果一个窗口不是顶级窗口,但有WS_EX_APPWINDOW风格,那么Windows将为它在任务栏上创建一个按钮,否则不会有相应的任务栏按钮.

3.如果一个窗口是顶级窗口,但加了WS_EX_TOOLWINDOW,并去掉WS_EX_APPWINDOW风格,那么Windows不会为它在任务栏上创建一个按钮.

4....

  知道了这些规则,你就能随心所欲的控制你的窗口是否要ShowInTaskbar了,下面给出一个问题和解决方案.

问题:在MFC中一个基于对话框的工程,想要主对话框不在任务栏上显示按钮,该如何做?

解决方案1:

根椐上面的第三条规则,给该对话框加上WS_EX_TOOLWINDOW风格并去掉WS_EX_APPWINDOW风格.

代码:

在对话框类的OnInitDialog函数里加入:
ModifyStyleEx(WS_EX_APPWINDOW,WS_EX_TOOLWINDOW,SWP_FRAMECHANGED);


备注:这种方法会有一个缺点,你的对话框的标题将会是一个小标题,所以大多数人不会采用这种方法.

解决方案2:

根椐上面的第二条规则,将该对话框变为非顶级窗口并去掉WS_EX_APPWINDOW,在app类的initinstance里的主对话框domodal之前生成一个隐藏的对话框,并将这个窗口的指针保存在CWinApp类的m_pMainWnd成员变量中,以后DoModal的对话框都将是它的子窗口.


代码:

在app的initinstance函数中加入以下加注释的代码
BOOL CTestDlgApp::InitInstance()
{
  ...
  CFrameWnd mainWnd; //生成一个框架窗口对象
  mainWnd.Create(NULL,"aa");//生成窗口,不带ws_visible,也就是说不会显示出来

  CTestDlgDlg dlg;
  m_pMainWnd = &mainWnd;//将这个隐藏窗口设为主窗口

  int nResponse = dlg.DoModal();
  ...
}

在对话框的OnInitDialog里加入 ModifyStyleEx(WS_EX_APPWINDOW,0);以便出掉WS_EX_APPWINDOW。

备注:这种方法较为麻烦,不过很实用,据我观察,C++Builder和Delphi生成的程序都应该有一个隐藏的窗口.


解决方案3:

在IE版本为4.0以上的系统中有一个叫ITaskbarList的接口,故名思义,这个接口可用来控制任务栏上的按钮是否显示,你只要给出一个窗口的句柄和是否显示,完全不需考虑上面那此乱七八糟的规则.

代码:

void ShowInTaskbar(HWND m_hWnd,BOOL bshow)
{
//在app的InitInstance中加入::CoInitialize(NULL);
HRESULT hr;
ITaskbarList *pTaskbarList;

hr=CoCreateInstance(CLSID_TaskbarList,NULL,CLSCTX_INPROC_SERVER,
IID_ITaskbarList,(void**)&pTaskbarList);

pTaskbarList->HrInit();//MSDN中说在使用这个接口的其它方法之前得调用这个方法以便做一些初始化动作,我发现不调用这个方法也可以的.
if(bshow){
pTaskbarList->AddTab(m_hWnd);
}
else{
pTaskbarList->DeleteTab(m_hWnd);
}
pTaskbarList->Release();
//在app的ExitInstance中加入::CoUninitialize();

}


在需要的时候调用这个函数就可以了,第一个参数为窗口的句柄,第二个参数指明是在任务栏上是否显示按钮.

备注:这种方法功能强大,使用也较为简便,不过低版本的windows可能不支持.

以上三种方案大家可根据情况自行选择.

Wednesday, May 26, 2004

 

How to implement Full Screen function

1.
For Doc/View program

(按:很老的回顾贴,但是很实用的文章。我的程序全屏显示功能就是在这基础上完成的。效果还不错。其配套代码比较详细,记下了菜单等的位置,并进行变换。只是要注意SDI和MDI实现时的细微差别)

from http://www.programfan.com/showarticle.asp?id=2497

全屏显示是一些应用软件程序必不可少的功能。比如在用VC++编辑工程源文件或编辑对话框等资源时,选择菜单“ViewFull Screen”,即可进入全屏显示状态,按“Esc”键后会退出全屏显示状态。
  在VC++6.0中我们用AppWizard按默认方式生成单文档界面的应用程序框架。下面将先讨论点击菜单项“ViewFull Screen”实现全屏显示的方法,再讲述按“Esc”键后如何退出全屏显示状态。
  1) 在CMainFrame类中,增加如下三个成员变量。
  Class CMainFrame : public CFrameWnd
  { private: //自己添加的三个成员变量
  WINDOWPLACEMENT m_OldWndPlacement; //用来保存原窗口位置
  BOOL m_bFullScreen; //全屏显示标志
  Crect m_FullScreenRect; //表示全屏显示时的窗口位置
  protected: CMainFrame();
  DECLARE_DYNCREATE(CMainFrame)}
  2)在资源编辑器中编辑菜单IDR_MAINFRAME。在“View”菜单栏下添加菜单项“Full Screen”。在其属性框中,ID设置为ID_FULL_SCREEN,Caption为“Full Screen”。还可以在工具栏中添加新的工具图标,并使之与菜单项“Full Screen”相关联,即将其ID值也设置为ID_FULL_SCREEN。
  3)设计全屏显示处理函数,在CMainFrame类增加上述菜单项ID_FULL_SCREEN消息的响应函数。响应函数如下:
  void CMainFrame::OnFullScreen()
  {GetWindowPlacement(&m_OldWndPlacement);
  Crect WindowRect;
  GetWindowRect(&WindowRect);
  Crect ClientRect;
  RepositionBars(0, 0xffff, AFX_IDW_PANE_FIRST, reposQuery, &ClientRect);
  ClientToScreen(&ClientRect);
  // 获取屏幕的分辨率
  int nFullWidth=GetSystemMetrics(SM_CXSCREEN);
  int nFullHeight=GetSystemMetrics(SM_CYSCREEN);
  // 将除控制条外的客户区全屏显示到从(0,0)到(nFullWidth, nFullHeight)区域, 将(0,0)和(nFullWidth, nFullHeight)两个点外扩充原窗口和除控制条之外的 客户区位置间的差值, 就得到全屏显示的窗口位置
  m_FullScreenRect.left=WindowRect.left-ClientRect.left;
  m_FullScreenRect.top=WindowRect.top-ClientRect.top;
  m_FullScreenRect.right=WindowRect.right-ClientRect.right+nFullWidth;
  m_FullScreenRect.bottom=WindowRect.bottom-ClientRect.bottom+nFullHeight;
  m_bFullScreen=TRUE; // 设置全屏显示标志为 TRUE
  // 进入全屏显示状态
  WINDOWPLACEMENT wndpl;
  wndpl.length=sizeof(WINDOWPLACEMENT);
  wndpl.flags=0;
  wndpl.showCmd=SW_SHOWNORMAL;
  wndpl.rcNormalPosition=m_FullScreenRect;
  SetWindowPlacement(&wndpl);}
  4)重载CMainFrame类的OnGetMinMaxInfo函数,在全屏显示时提供全屏显示的位置信息。
  Void CMainFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI)
  {if(m_bFullScreen)
  {lpMMI->ptMaxSize.x=m_FullScreenRect.Width();
  lpMMI->ptMaxSize.y=m_FullScreenRect.Height();
  lpMMI->ptMaxPosition.x=m_FullScreenRect.Width();
  lpMMI->ptMaxPosition.y=m_FullScreenRect.Height();
  // 最大的Track尺寸也要改变
  lpMMI->ptMaxTrackSize.x=m_FullScreenRect.Width();
  lpMMI->ptMaxTrackSize.y=m_FullScreenRect.Height();
  }CFrameWnd::OnGetMinMaxInfo(lpMMI) ;
  }完成上面的编程后,可以联编执行FullScreen.exe,选择菜单“ViewFull Screen”或点击与之关联的工具栏按钮即可进入全屏显示状态。但现在还需要增加用户退出全屏显示状态的操作接口,下面讲述如何编程实现按“Esc”键退出全屏显示状态。
  1)在ClassView中选中CMainFrame并单击鼠标右键,选择“Add Member Function…”,添加public类型的成员函数EndFullScreen,该函数将完成退出全屏显示的操作。
  Void CMainFrame::EndFullScreen()
  {if(m_bFullScreen)
  {// 退出全屏显示, 恢复原窗口显示
  ShowWindow(SW_HIDE);
  SetWindowPlacement(&m_OldWndPlace
ment);}}
  2)函数EndFullScreen可以退出全屏显示状态,问题是如何在“Esc”键被按下之后调用执行此函数。由于视图类可以处理键盘输入的有关消息(如WM_KEYDOWN表示用户按下了某一个键),我们将在视图类CFullScreenView中添加处理按键消息WM_KEYDOWN的响应函数OnKeyDown。判断如果按的键为“Esc”键,则调用CMainFrame类的函数EndFullScreen,便可退出全屏显示状态。
  Void CFullScreenView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
  {if(nChar==VK_ESCAPE) // 如果按的键为Esc键
  {// 获取主框架窗口的指针
  CMainFrame *pFrame=(CMainFrame*)AfxGetApp()->m_pMainWnd;
  // 调用主窗口类的自定义函数 EndFullScreen ,便可退出全屏显示状态
  pFrame->EndFullScreen();}
  Cview::OnKeyDown(nChar, nRepCnt, nFlags);}
  这样我们就实现了比较专业的全屏显示的功能,相信肯定会令你设计的软件程序增色不少。

(按:编程爱好者杂志还是挺不错的,入门的时候每期都看,呵呵)

2.
for dialog program

/////////////////////使对话框最大化,并去掉边框,标题栏//////////

// Use window set style function

::SetWindowLong
(
this->m_hWnd,
GWL_STYLE,
WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_SYSMENU | DS_3DLOOK | DS_SETFONT
);

::SetWindowLong
(
this->m_hWnd,
GWL_EXSTYLE,
WS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR | WS_EX_CONTROLPARENT |WS_EX_APPWINDOW
);
::ShowWindow
(
this->m_hWnd,
SW_MAXIMIZE
);


///////////////SendMessage(WM_SYSCOMMAND,SC_MAXIMIZE,NULL)///////
int cx, cy;
HDC dc = ::GetDC(NULL);
cx = GetDeviceCaps(dc,HORZRES) +
GetSystemMetrics(SM_CXBORDER);
cy = GetDeviceCaps(dc,VERTRES) +
GetSystemMetrics(SM_CYBORDER);
::ReleaseDC(0,dc);

//去除标题和边框
SetWindowLong(m_hWnd, GWL_STYLE,
GetWindowLong(m_hWnd, GWL_STYLE) &
(~(WS_CAPTION | WS_BORDER)));

// 置对话框为最顶端并扩充到整个屏幕
::SetWindowPos(m_hWnd, HWND_TOPMOST,
-(GetSystemMetrics(SM_CXBORDER)+1),
-(GetSystemMetrics(SM_CYBORDER)+1),
cx+1,cy+1, SWP_NOZORDER);
////////////////////////////////////////////////////////////////

Tuesday, May 25, 2004

 

Some notes on memcpy

这个问题是看了袁峰blog上的讨论
http://blog.joycode.com/fyuan/articles/30945.aspx
而来的。其实,Memcpy一直是大家讨论的热点。这里也算老贴回顾,来两篇文章。

1.
Optimizing Memcpy improves speed
by Michael Morrow
from http://www.embedded.com/showArticle.jhtml?articleID=19205567

Knowing a few details about your system-memory size, cache type, and bus width can pay big dividends in higher performance.

The memcpy() routine in every C library moves blocks of memory of arbitrary size. It's used quite a bit in some programs and so is a natural target for optimization. Cross-compiler vendors generally include a precompiled set of standard class libraries, including a basic implementation of memcpy(). Unfortunately, since this same code must run on hardware with a variety of processors and memory architectures, it can't be optimized for any specific architecture. An intimate knowledge of your target hardware and memory-transfer needs can help you write a much more efficient implementation of memcpy().

This article will show you how to find the best algorithm for optimizing the memcpy() library routine on your hardware. I'll discuss three popular algorithms for moving data within memory and some factors that should help you choose the best algorithm for your needs. Although I used an Intel XScale 80200 processor and evaluation board for this study, the results are general and can be applied to any hardware.

A variety of hardware and software factors might affect your decision about a memcpy() algorithm. These include the speed of your processor, the width of your memory bus, the availability and features of a data cache, and the size and alignment of the memory transfers your application will make. I'll show you how each of these factors affects the performance of the three algorithms. But let's first discuss the algorithms themselves.

Three basic memcpy() algorithms
The simplest memory-transfer algorithm just reads one byte at a time and writes that byte before reading the next. We'll call this algorithm byte-by-byte. Listing 1 shows the C code for this algorithm. As you can see, it has the advantage of implementation simplicity. Byte-by-byte, however, may not offer optimal performance, particularly if your memory bus is wider than 8 bits.

Listing 1: The byte-by-byte algorithm

void * memcpy(void * dst, void const * src, size_t len)
{
char * pDst = (char *) dst;
char const * pSrc = (char const *) src;


while (len--)
{
*pDst++ = *pSrc++;
}

return (dst);
}

An algorithm that offers better performance on wider memory buses, such as the one on the evaluation board I used, can be found in GNU's newlib source code. I've posted the code here. If the source and destination pointers are both aligned on 4-byte boundaries, my modified-GNU algorithm copies 32 bits at a time rather than 8 bits. Listing 2 shows an implementation of this algorithm.

Listing 2: The modified-GNU algorithm

void * memcpy(void * dst, void const * src, size_t len)
{
long * plDst = (long *) dst;
long const * plSrc = (long const *) src;

if (!(src & 0xFFFFFFFC) && !(dst & 0xFFFFFFFC))
{
while (len >= 4)
{
*plDst++ = *plSrc++;
len -= 4;
}
}

char * pcDst = (char *) plDst;
char const * pcDst = (char const *) plSrc;


while (len--)
{
*pcDst++ = *pcSrc++;
}

return (dst);
}

A variation of the modified-GNU algorithm uses computation to adjust for address misalignment. I'll call this algorithm the optimized algorithm. The optimized algorithm attempts to access memory efficiently, using 4-byte or larger reads-writes. It operates on the data internally to get the right bytes into the appropriate places.

Note that the optimized algorithm uses some XScale assembly language. The preload instruction is a hint to the ARM processor that data at a specified address may be needed soon. Processor-specific opcodes like these can help wring every bit of performance out of a critical routine. Knowing your target machine is a virtue when optimizing memcpy().

Having looked at all three of the algorithms in some detail, we can begin to compare their performance under various conditions.

Block size
What effect does data size have on the performance of our algorithms? To keep things simple, let's assume that there's no data cache (or that it's been disabled) and that all of the source and destination addresses are aligned on 4-byte boundaries.

Note also that the performance of byte-by-byte improves dramatically as the processor clock speed increases. This implies that the routine is CPU-bound. Until we saturate the memory bus with reads and writes, the byte-by-byte algorithm will continue to execute more quickly.

Here the modified-GNU algorithm has the best performance, as it makes more efficient use of the memory bus. The optimized algorithm has comparable performance, but the effects of its additional computation take their toll in overhead.

Data alignment
What happens if the source and destination are not both aligned on a 4-byte boundary? The modified-GNU algorithm performs worse than byte-by-byte in this situation, largely because it defaults to byte-by-byte (after a fixed overhead).

The big overhead in the GNU algorithm comes from register save-restore in its prologue-epilogue. The algorithm saves off four registers, where the byte-by-byte routine saves none. So, as memory speed decreases in relation to processor speed, the GNU algorithm suffers accordingly. By the way, "optimized" memcpy saves off nine registers, which is part of the reason it becomes less compelling at high core and bus speeds. This overhead matters less when the stack is cached (probably the normal case).

The optimized algorithm handles unaligned addresses the best outperforming byte-by-byte. At slower clock speeds, the overhead of dealing with alignment is amortized by the cost of actually moving the memory four times as quickly. As CPU performance improves (with respect to the memory system), the elegance of an algorithm like optimized becomes less helpful.

Caching
Everything changes if your processor has a data cache. Let's try the same memcpy tests we've already run, but with the data already in cache.

Also, the data for unaligned memcpy shows that the GNU memcpy performance degrades to that of byte-by-byte performance when addresses are not aligned. You may see severe degradation in memcpy performance if your data is not always aligned in memory.

Write policy
A write-through cache is one that updates both the cache and the memory behind it whenever the processor writes. This sort of cache tries to satisfy reads without going to memory.

A write-back cache, on the other hand, tries to satisfy both reads and writes without going to memory. Only when the cache needs storage will it evict some of its data to memory; this is called variously a write back, a cast out, or an eviction. Write-back caches tend to use less memory bandwidth than write-through caches.

The processor I used allows the cache to be configured using either policy. What effect does this have on memcpy? It depends. With a cold cache, optimized memcpy with write-back cache works best because the cache doesn't have to write to memory and so avoids any delays on the bus.

For a garbage-filled cache, write-through caches work slightly better, because the cache doesn't need to spend extra cycles evicting irrelevant data to memory. As usual, the more you know about your system"such as the likelihood of having certain data in the cache"the better you can judge the efficacy of one cache policy over another.

Special situations
If you know all about the data you're copying as well as the environment in which memcpy runs, you may be able to create a specialized version that runs very fast. Figure 9 shows the performance gain we accrue by writing a memcpy that handles only 4KB-aligned pages when the cache is in write-through mode. This example shows that writing a very specific algorithm may double the speed of a memcpy-rich program. I've posted one of these algorithms here.

Optimize away
Some applications spend significant processor time transferring data within memory; by choosing the optimal algorithm, you could improve overall program performance significantly. The moral of the story: know your target hardware and the characteristics of your application. Armed with this knowledge, you can easily find the optimal algorithm.

(按:这几天还在组里讨论ARM芯片的国产化问题呢。都说支持国产,可龙芯那样的实在有点高不成,低不就的感觉)

2.
http://www.boost.org/more/generic_programming.html
这篇文档相信看过的人很多。看看Boost的代码就可以知道他们对template的memcopy做了什么样的优化

现在,我想大家都明白为什么袁峰的code定义要快一点了吧。那就是让complier优化选取按多大的宽度进行拷贝。

3.
但是这样也会带来安全性的一些问题,比如

为什么C语言的strcpy函数有漏洞 czy原创于03.04
from http://bbs.nsfocus.net/index.php?act=ST&f=3&t=144495

前言:
研究了几天DOS下的溢出原理,最后明白了其实原理都很简单
关键是要懂得为什么C语言的strcpy函数有漏洞,为什么对这个函
数的不正常使用会造成溢出.

一节:介绍strcpy函数
能看到这篇文章的人可能都知道问题很多是出在它的身上吧呵呵。
先看一看在标准的C语言的string.h中对这个函数的申明
char *_Cdecl stpcpy (char *dest, const char *src);
对于代码看下面的:(这是微软对这个函数的说明)
(%VC%/vc7/crt/src/intel/strcat.asm)
;***
;char *strcpy(dst, src) - copy one string over another
;Purpose:
; Copies the string src into the spot specified by
; dest; assumes enough room.
;
; Algorithm:
; char * strcpy (char * dst, char * src)
; {
; char * cp = dst;
; while( *cp++ = *src++ ); /* Copy src over dst */
; return( dst );
; }
;Entry:
; char * dst - string over which "src" is to be copied
; const char * src - string to be copied over "dst"
;
;Exit:
; The address of "dst" in EAX
;
;Uses:
; EAX, ECX
;
;Exceptions:
;**********************************************************************

本来想去掉一些注解,不过觉得还是留着好哈:)
从上面我们可以看到这样的代码有问题有:
1.没有检查输入的两个指针是否有效。
2.没有检查两个字符串是否以NULL结尾。
3.没有检查目标指针的空间是否大于等于原字符串的空间。

好了现在我们知道了对于调用string.h中的这个函数,和我们自已写一个如下的程序
没有本质上的区别那么我们就来研究它就可以了.
就叫它c4.exe吧.
main(){j();}
j()
{
char a[]={'a','b','\0'};
char b[1];
char *c=a;
char *d=b;
while(*d++=*c++);
printf("%s\n",b);
}


二节:调试我们的c4.exe
所用工具W32dasm,debug,tcc,tc
第一步我们用TC2编绎生成可执行文件c4.exe.
第二步用TCC -B生成这段C代码的汇编源代码.
第三步用W32dasm和debug对c4.exe进行静态和动态调试

先分析由TCC生成的c4.asm代码如下:
先说明一下由于这是一个完整的包括了MAIN函数的C程序
程序刚开始时数据段和堆栈段还有代码都不在一起但是当
执行到我们的J函数时堆栈和数段就在一起了这要特别注意.

ifndef ??version
?debug macro
endm
endif
?debug S "c4.c"

_TEXT segment byte public 'CODE'
DGROUP group _DATA,_BSS
assume cs:_TEXT,ds:DGROUP,ss:DGROUP
_TEXT ends

_DATA segment word public 'DATA'
d@ label byte
d@w label word
_DATA ends

_BSS segment word public 'BSS'
b@ label byte
b@w label word
?debug C E930A68D2E0463342E63
_BSS ends


_TEXT segment byte public 'CODE'
; ?debug L 1
_main proc near
; ?debug L 3
call near ptr _j //这儿执行我们的J函数
@1:
; ?debug L 4
ret
_main endp
_TEXT ends

_DATA segment word public 'DATA' //最先在数据段中定义我们的源串ab结尾符\0
db 97
db 98
db 0
_DATA ends


_TEXT segment byte public 'CODE'
; ?debug L 6
_j proc near
push bp //J函数入口
mov bp,sp
sub sp,6
push si
push di
push ss
lea ax,word ptr [bp-6]
push ax
push ds
mov ax,offset DGROUP:d@ //特别注意这是得到源串在数据段中的偏移
push ax //所有SCOPY@以上的代码的作用是在堆栈中分配源串加目的串那么多个空间
mov cx,3 //cx=3指定要拷贝的字符数
call far ptr SCOPY@ //执行了另一个函数作用是把数据段中的源串拷到栈中
; ?debug L 10
lea si,word ptr [bp-6]
; ?debug L 11
lea di,word ptr [bp-2]
; ?debug L 12
jmp short @3
@5:
@3:
; ?debug L 12
mov bx,si
inc si
mov al,byte ptr [bx]
mov bx,di
inc di
mov byte ptr [bx],al
or al,al
jne @5
@4:
; ?debug L 13
lea ax,word ptr [bp-2]
push ax
mov ax,offset DGROUP:s@ //得到printf函数的打印格式参数
push ax
call near ptr _printf
pop cx
pop cx
@2:
; ?debug L 14
pop di
pop si
mov sp,bp
pop bp
ret
_j endp
_TEXT ends
?debug C E9

_DATA segment word public 'DATA'
s@ label byte
db 37 //%
db 115 //s
db 10 //换行符:)
db 0
_DATA ends
extrn SCOPY@:far
_TEXT segment byte public 'CODE'
extrn _printf:near
_TEXT ends
public _main
public _j
end


三节:分析W32Dasm得来的静态汇编代码,也就是程序最终的代码同时我们一步步来分析
这时堆栈的情况.
文章写到这儿可能大家一定认识都是些看到就头大的代码吧,没事我先分析一下
这些代码就执行来说可以分为三个部分:

1.从01FE到020B是根据C代码中的定义在堆栈中分配空间例子中分了6个字节,定义多少分多少也没有毛病
2远跳到0000:1395是把数据段中的源串放到堆栈中由于放入个数在cx中所以这儿也没有毛病
3在堆栈中把源串拷到目的串所在的内存单元中问题就在这儿了!


:0001.01FA E80100 call 01FE //执行我们的j函数
:0001.01FD C3 ret

:0001.01FE 55 push bp
:0001.01FF 8BEC mov bp, sp
:0001.0201 83EC06 sub sp, 0006
:0001.0204 56 push si
:0001.0205 57 push di
:0001.0206 16 push ss
:0001.0207 8D46FA lea ax, [bp-06]
:0001.020A 50 push ax
:0001.020B 1E push ds
:0001.020C B89401 mov ax, 0194
:0001.020F 50 push ax
:0001.0210 B90300 mov cx, 0003
:0001.0213 9A95130000 call 0000:1395 //这儿先跳到1395去执行了由于它是在0000所以是远跳

:0001.0218 8D76FA lea si, [bp-06]
:0001.021B 8D7EFE lea di, [bp-02]
:0001.021E EB00 jmp 0220
:0001.0220 8BDE mov bx, si
:0001.0222 46 inc si
:0001.0223 8A07 mov al , [bx]
:0001.0225 8BDF mov bx, di
:0001.0227 47 inc di
:0001.0228 8807 mov [bx], al
:0001.022A 0AC0 or al , al
:0001.022C 75F2 jne 0220
:0001.022E 8D46FE lea ax, [bp-02]
:0001.0231 50 push ax
:0001.0232 B89701 mov ax, 0197
:0001.0235 50 push ax
:0001.0236 E8BC08 call 0AF5 //执行打印输出
:0001.0239 59 pop cx
:0001.023A 59 pop cx
:0001.023B 5F pop di
:0001.023C 5E pop si
:0001.023D 8BE5 mov sp, bp
:0001.023F 5D pop bp
:0001.0240 C3 ret
//下面的就是我们的SCOPY@
0001.1395 55 push bp
:0001.1396 8BEC mov bp, sp
:0001.1398 56 push si
:0001.1399 57 push di
:0001.139A 1E push ds
:0001.139B C57606 lds si, [bp+06]
:0001.139E C47E0A les di, [bp+0A]
:0001.13A1 FC cld
:0001.13A2 D1E9 shr cx, 01
:0001.13A4 F3 repz
:0001.13A5 A5 movsw
:0001.13A6 13C9 adc cx, cx
:0001.13A8 F3 repz
:0001.13A9 A4 movsb
:0001.13AA 1F pop ds
:0001.13AB 5F pop di
:0001.13AC 5E pop si
:0001.13AD 5D pop bp
:0001.13AE CA0800 retf 0008

我们现在开始DEBUG动态调试:
第一步D:\turboc2>debug c4.exe
-g 01FE 通过W32DASM中的查找我们直接跳到J入口处执行

AX=0000 BX=0566 CX=000E DX=067F SP=FFE8 BP=FFF4 SI=00D8 DI=054B
DS=13DB ES=13DB SS=13DB CS=129F IP=01FE NV UP EI PL ZR NA PE NC
129F:01FE 55 PUSH BP
-t

AX=0000 BX=0566 CX=000E DX=1193 SP=FFE6 BP=FFF4 SI=00D8 DI=054B
DS=13DB ES=13DB SS=13DB CS=129F IP=01FF NV UP EI PL ZR NA PE NC
129F:01FF 8BEC MOV BP,SP


由于上一条指令是CALL O1FE,所以也就有一条POP 01FD,然后又是一个PUSH BP
-d ss:ffe0
13DB:FFE0 FF 01 9F 12 F3 0B F4 FF-FD 01 1D 01 01 00 F2 FF ................
13DB:FFF0 54 05 F6 FF 00 00 43 35-2E 45 58 45 00 00 FB 00 T.....C5.EXE....

现在就来看看栈的情况
mov bp,sp后BP就成了FFE6


FFE0 | ->SUB SP,0006(空了六个字节为源目的串在堆栈中分配了空间)
FFE1 |
FFE2 |
FFE3 |
FFE4 |
FFE5 |
FFE6 |F4 ---->当前的栈顶FFE6
FFE7 |FF ---->原BP
FFE8 |FD
FFE9 |01
FFEA |



然后把si,di,ss压入堆栈,这时SP就变成了FFDA
再执行lea ax,[bp-06]
push ax
push ds
这是把分配的内存空间的内存地址也放到堆栈中,还有DS
然后又执行
mov ax,0194(mov ax,offset DGROUP:d@) 得到字串在数据段中的偏移
push ax
mov cx,03
好了该执行我们的SCOPY@了
CALL 0000:1395 由于是一个远跳所以CS IP都压堆栈了
再来看看堆栈的情况

内存低地址

FFD0 |18 ---->ip,也就是lea si,[bp-06]所在的CS段中的偏移
FFD1 |02
FFD2 |9F ---->先压CS
FFD3 |12
FFD4 |94 ---->字串在数据段中的偏移压栈
FFD5 |01
FFD6 |DB ---->DS
FFD7 |13
FFD8 |EO ---->为字串分配的空间的地址
FFD9 |FF
FFDA |DB ---->SS
FFDB |13
FFDC |DI ---->这儿把DI,SI压入堆栈的目的是因为过会儿把数
FFDD | 据段中的数据般到堆栈时又要用到它们所以要先保存
FFDE |SI
FFDF |
FFE0 |1 ->SUB SP,0006(空了六个字节为源,目的串在堆栈中分配了空间)
FFE1 | 2
FFE2 | 3
FFE3 | 4
FFE4 | 5
FFE5 | 6
FFE6 |F4 ---->当前的栈顶FFE6
FFE7 |FF 原BP为FFF4,现在BP为FFE6
FFE8 |FD ---->j执行完后返回地址
FFE9 |01
FFEA |

内存高地址
好了到这儿我们的分析算是完成1/3了,现在就来看看到底SCOPY是怎么把数据段中的字串
放到堆栈中去的.

push bp 把以前的BP(FFE6)压栈
mov bp,sp 当前sp=bp=FFCE
push si
push di
push ds 这时sp为FFC8
然后执行
lds si,[bp+06] si就等于ffce+06=ffd4,ffd4中的数据就是字串在数据段中的偏移0194
les di,[bp+0a] di就等于ffc3+0a=ffd8,ffd8中的数据就是堆栈中存放字串的首地址ffe0
这两条指令执行完后si=0194,di=ffe0
内存低地址
下面是栈顶情况:

FFC8 |DB -->ds压栈 <--sp=ffc8
FFC9 |13
FFCA |DI
FFCB |
FFCC |SI
FFCD |
FFCE |E6
FFCF |FF

内存高地址

下面的7行代码就简单了把SI指向的地址中的数据移到DI指的地址中去
cld
shr cx,01 (CX等于3)
repz
movsw
adc cx,cx
repz
movsb

这样是较率比较高的移法了先一次移两个用MOVSW指令,当只一个时用MOVSB
上面的指令执行完后,堆栈中的
FFE0 就分别成了 a
FFE1 b
FFE2 0

好了数据般完了,该还原DS,DI,SI,BP了
pop ds
pop di
pop si
pop bp
这四条指令执行完后sp=FFD0,bp还原成了以前的FFE6
最后是返回指令
retf 8
对这个指令要好好就一下:由于是远跳来执行的所以sp要加4(ffd0+4=ffd4)
再加上代参数8所以还要加8(ffd4+8=ffdc)

这时堆栈的情况就成了:
SP=FFDC,BP=FFE6

FFDC |DI ---->这儿把DI,SI压入堆栈的目的是因为过会儿把数
FFDD | 据段中的数据般到堆栈时又要用到它们所以要先保存
FFDE |SI
FFDF |
FFE0 |1 a ->SUB SP,0006(空了六个字节为源,目的串在堆栈中分配了空间)
FFE1 | 2 b
FFE2 | 3 0(注意源字串已经正确的放在了堆栈中!)
FFE3 | 4
FFE4 | 5
FFE5 | 6
FFE6 |F4 ---->当前的栈顶FFE6
FFE7 |FF 原BP为FFF4,现在BP为FFE6
FFE8 |FD ---->j执行完后返回地址
FFE9 |01


好了该总结一下了:
从上面我们就可以看出上面这些都是没有毛病的.
为什么要分配六个字节的空间?
先来看看我在C程序中是怎么定义的:
char a[]={'a','b','\0'};
char b[1];
其实C中分配空间的规则是很简单的,就是每一个串的长度一定要是双数
如果为单就加1
象上面的: 源为3+1=4
目的1+1=2,源+目的=4+2=6

个人认为这个地方对于要正确的溢出是很重要的,因为有的文章里面说
多一个字节可以了但真的是这样吗?不一定吧,象我例子中就是这样的!

不多说了看代码吧:
lea si,[bp-06] 这时BP=FFE6,FFE6-06=FFE0
lea di,[bp-02] 同样DI就等于FFE4
设好了SI,DI后,就是一个循环了一次一个字节的把源串中的字母放到目的串中
下面的代码是最重要的了,问题也就出在这儿:!!!!!!

jmp 0220
0220 mov bx,si (把SI的地址给BX)
inc si (SI地址加1)
mov al,[bx] (把BX寄存器中记录的内存地址中的数据给al,第一次就是取出a)
mov bx,di (把dI的地址给BX)
inc di (dI地址加1)
mov [bx],al (把AL中的字符给BX指向的地址)
or al,al
jne 0220 (不为0则跳)

------------又来看看栈的情况--------BP=FFE6,SP=SP=FFDC
FFDC |DI
FFDD |
FFDE |SI
FFDF |
FFE0 |a
FFE1 |b
FFE2 |0
FFE3 |
FFE4 |a (第一次执行DI=FFE4,FFE4中的值成了a)
FFE5 |

从上面的代码我想你已经看出问题来了,是否拷完代码的判断条件只是看有没有遇到\0
而并没有去比较源串的大小是否比目的串大!


FFE0 |a
FFE1 |b
FFE2 |0
FFE3 |
FFE4 |a (第一次执行DI=FFE4,FFE4中的值成了a)
FFE5 |b
FFE6 |F4 ---->原始BP (注意在我的例子中FFE6将变成0,
FFE7 |FF 但这个只是保存的上个函数的BP所以程序还没出错)
FFE8 |FD ---->main函返回地址
FFE9 |01

再看后来的代码:


lea ax, [bp-02]
mov ax, 0197
push ax
call 0AF5 //执行打印输出
pop cx
pop cx //上面的几行就是打印出目的串

pop di
pop si //把DI,SI弹出
mov sp, bp
pop bp
ret

hurk 在后面接道

现在明白了^-^
====================vul.c======================
#include
Foo(char* s)
{
char buf[16]="";
strcpy(buf, s);
printf("The input String is %s\n", buf);
}
main(int argc, char* argv[])
{
if(argc == 2)
{
Foo(argv[1]);
}
else
{
printf("Usage: %s < string>\n", argv[0]);
}

}
=================end======================

以Debug模式编译:

shanghai =>gcc vul.c -o vul -g

再用gdb运行它:

shanghai =>gdb vul
GNU gdb 5.2
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and
you are welcome to change it and/or distribute copies of it under
certain conditions. Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for
details. This GDB was configured as "i386-pc-solaris2.8"...
/*
在程序入口处main设置一个断点
*/
(gdb) b main
Breakpoint 1 at 0x8050956: file vul.c, line 12.
/*
我以一个字符串--10个'A'作为程序vul的输入参数。
*/
(gdb) r AAAAAAAAAA
Starting program: /export/home/moda/buf_of/vul AAAAAAAAAA
Breakpoint 1, m12 if(argc == 2)
/*
程序在main处中断,我们接着单步执行(Single Step)到Foo
*/
(gdb) s
14 Foo(argv[1]);
/*
程序即将要进入Foo,我们看看将要执行的汇编指令。
*/
(gdb) x/10i $eip
0x805095c : add $0xfffffff4,%esp
0x805095f : mov 0xc(%ebp),%eax
0x8050962 : add $0x4,%eax
0x8050967 : mov (%eax),%edx
0x8050969 : push %edx
0x805096a : call 0x8050904
0x805096f : add $0x10,%esp
0x8050972 : jmp 0x805098a
0x8050974 : add $0xfffffff8,%esp
0x8050977 : mov 0xc(%ebp),%eax
/*
函数Foo在地址0x805096a处被main调用:"call 0x8050904
"。而0x805096f为Foo调用后的返回地址,大家记住这个地址,因为下面要提到它。

接着我们用si(Single
Instruction)一个指令一个指令地执行
*/
(gdb) si
0x0805095f 14 Foo(argv[1]);
(gdb) si
0x08050962 14 Foo(argv[1]);
(gdb) si
0x08050967 14 Foo(argv[1]);
(gdb) si
0x08050969 14 Foo(argv[1]);
(gdb) si
0x0805096a 14 Foo(argv[1]);
/*
在这里程序即将要进入Foo,我们看看寄存器当前的内容:
*/
(gdb) i reg
eax 0x8047bf8 134511608
ecx 0x0 0
edx 0x8047d0d 134511885
ebx 0xdfbfb000 -541085696
esp 0x8047bb4 0x8047bb4
ebp 0x8047bcc 0x8047bcc
esi 0x8047bb0 134511536
edi 0x8047c74 134511732
eip 0x805096a 0x805096a
eflags 0x202 514
cs 0x17 23
ss 0x1f 31
ds 0x1f 31
es 0x1f 31
fs 0x0 0
gs 0x0 0
fctrl 0x137f 4991
fstat 0x0 0
ftag 0xffff 65535
fiseg 0x0 0
fioff 0x0 0
foseg 0x0 0
fooff 0x0 0
---Type to continue, or q to quit---q
Quit
/*
在寄存器ESP中的是当前堆栈栈顶指针0x8047bb4,
而EBP为main函数堆栈栈底指针0x8047bcc,这个0x8047bcc是要大家记住的第二个地址

继续向下执行
*/
(gdb) si
Foo (s=0x2
) at vul.c:4
4 {
(gdb) x/5i $eip
0x8050904 : push %ebp
0x8050905 : mov %esp,%ebp
0x8050907 : sub $0x18,%esp
0x805090a : mov 0x8050a28,%al
0x805090f : mov %al,0xfffffff0(%ebp)
(gdb) s
Foo (s=0x8047d0d "AAAAAAAAAA") at vul.c:5
5 char buf[16]="";
(gdb) s
6 strcpy(buf, s);
(gdb) s
7 printf("The input String is %s\n", buf);
/*
程序进入Foo中,并把输入的字符串拷贝到了缓冲区buf中。我们来看一下当前堆栈的内容:
*/
(gdb) x/20x $esp
0x8047b94: 0xdfb35d90 0x00000210 0x41414141
0x41414141
0x8047ba4: 0x00004141 0x00000000 0x08047bcc
0x0805096f
0x8047bb4: 0x08047d0d 0xdfb3d3a7 0xdfbf137f
0x08047bb0
0x8047bc4: 0xdfbfb000 0xdfbf137f 0x08047be8
0x0805081b
0x8047bd4: 0x00000002 0x08047bf4 0x08047c00
0x08050a10
省略以下部分。。。

从地址0x8047b9c到0x8047bab的区域是系统分配给buf的缓冲区,刚好16个字节,分别对应buf的16个字符;前面的10个字节已经填上了10个刚拷贝进来的字符'A'的ASCII码41。紧接着这16个字节的是0x08047bcc,就是我刚才要大家记住的调用函数main的堆栈栈底地址;再后面的是我要大家记住的被调用函数Foo的返回地址0x0805096f。

根据上面的分析我们可以画一个缓冲区在内存中的分布图:

|<--buf: 16 Byte-->|<--Calling Function $EBP: 4 Byte-->|<--Called
Function RetAddr: 4 Byte -->|

buf缓冲区被分配在堆栈中,而且是和重要的系统管理数据如:调用函数堆栈栈底地址、被调用函数的返回地址紧紧地靠在一块----这,就是悲剧DNA根源。

在Foo中,buf是strcpy拷贝字符串的目标,而源字符串为s。strcpy忠实地把源字符串s完整地拷贝到以buf为开端的内存后面
---
┣━┒ ; `.
┟━┃┍╄┓
┝─┃┣╈┤
┗━┘┗┸┛

This page is powered by Blogger. Isn't yours?