首页 目录页 热情软件屋 问专家

李海文选

完善VB的文件拖放功能

李海

本文发表在《PC Computing中
文版/电子与电脑》97年 6月16日6期

文件拖放(drag and drop) 已经成为Windows 的一个标准功能,比如,你可以从文件管理器(File Manager) 里用鼠标选中一个文件,然后按下左键不放,拖动鼠标,然后在记事本(Notepad)的窗口中松开鼠标的左键,记事本就会自动地打开这个文件。文件拖放功能在Windows 95中更是无所不在,例如删除一个文件的最简单的方法便是将这个文件拖放到回收箱(Recycle Bin)的图标上。时下流行的其他操作系统,象OS/2 Warp、Mac-OS,也都支持拖放操作。如果你想让自己设计的程序更容易使用,毫无疑问应该提供对文件拖放的支持。Visual Basic也提供了一种拖放功能,但同我们所说的文件拖放并不完全一样。VB的拖放功能仅限于VB程序内部,你用VB设计的程序既不能接受文件管理器拖来的文件,也不能将文件放到文件管理器中去。看来,要想让VB支持文件拖放功能,我们要动点脑筋了。

我们知道,当用户放下文件时,Windows会向相应的Windows程序发送WM_DROPFILES消息,而VB提供的标准事件中不包括这一消息,这是VB不能支持文件拖放的关键所在。要在VB中产生新的事件,编写VBX控制是最方便的手段之一。绝大多数VB设计者可能对使用VBX不会感到陌生,在VB专业版中包括了几个非常有用的VBX,它们极大地扩展了VB的开发能力,而从Internet和市场上,你可能会找到数以千计的VBX。但对于许多国内的用户来说,自己动手编写VBX可能还从来没有想过,所以我们得从ABC讲起。

一、设计VBX控制的属性和事件

VBX的开发一般是使用C语言或汇编语言来进行的,VB本身不能用来编写VBX控制,开发环境与运行环境的不同会使VBX的调试不太方便,这是VB的一个缺点。VBX开发所需的所有库文件(.LIB)、头文件(.H)和帮助文件(.HLP)都在VB专业版的CDK目录下。您可以使用Borland C++ 4.5或Visual C++ 1.52来编译本文的源程序。用BC++编写VBX时要注意一点:必须将Case-sensitive Link选项关掉,因为C/C++是对大小写敏感的,而VB的库文件不区分大小写,如果不关掉相应的选项,将无法链接生成VBX文件。

VBX文件,从本质上来说,是一种特殊的动态链接库(DLL)。它除了具备一般的DLL的功能,还应当包括三个特殊的过程。一个是VBInitCC,它用于VBX的初始化。当加入VBX 到VB的工程文件(.MAK)时,VB会首先调用这个过程。在这个过程中,VBX应当向系统进行注册(Register),说明该VBX一共包括几个控制部件,每个部件支持哪些属性(Property)和事件(Event),以及控制部件其它的基本特征,比如窗口类(Window Class)。另一个是VBTermCC,它是在卸载VBX时被调用的,主要用于释放VBX所占用的系统资源。还有一个是VBGetModelInfo,VB通过它获得有关VBX的信息。在后面,我们将结合程序来具体说明这三个过程。

一个VBX文件中可以包括一个或几个控制部件。这些控制部件是通过消息同VB进行数据交换的,其通讯方式如图1所示。

插图

VB的运行模块将Windows消息传递给VBX控制过程(Custom Control Proc.),同时将VB程序读写属性和调用方法(Method)的操作转换为相应的VBM消息并传递给控制过程。VBX控制过程可以处理所有收到的消息,对于未处理的消息应传递给VB默认的控制过程(Default Control Proc.)继续处理。同标准的Windows控制过程相比较,VBX控制过程的特点是处理VBM消息,其他的部分大致相同。VB通过HCTL句柄来区分不同VBX控制。除此以外,控制过程还要在适当的时候产生自定义的事件。

VBX设计的关键是实现自定义的属性、方法和事件。属性、方法和事件设计的合理与否是评价VBX好坏的最重要的标准。VB提供了二十余种标准属性和十余种标准事件给设计者,应尽可能使用这些标准属性和事件,一方面可以简化设计工作,另一方面可以减少差错,提高软件的可靠性。通常,一个控制至少支持以下五种标准属性:Top、Left、Height、Width和Name。对于方法,VB只允许VBX设计者实现几种标准的方法:AddItem、RemoveItem、Clear、Move、ZOrder、Refresh和SetFocus等,你可以赋予这些方法特别的含义,但不能创造方法。

文件拖放编程可以被称作“三步曲”。首先,利用DragAcceptFile函数表明VB窗口可以接受文件释放,然后接收WM_DROPFILES消息,利用该消息传来的短参数(wParam),利用DragQueryFile和DragQueryPoint查询有关拖放的具体信息,最后,调用DragFinish释放拖放操作占用的系统内存。我们的拖放控制显然应当包括一个事件,以通知VB程序文件被释放在了窗口内,我们称这个事件为DropFile。除了支持前述的五种标准属性,还应该提供以下几个属性:Enabled属性,表示VB的窗口是否接受文件释放;ListCount属性,表示用户释放的文件总数;FileList属性,这是个数组,代表所释放的文件名(含路径名)。此外,拖放控制还支持了VB的几个标准属性:Align、Tag、HelpContext、Parent。用户释放文件位置的横、纵坐标将作为DropFile事件的两个参数传递给VB。

下面就是我们程序的头文件DROFILE.H,其中包括了事件和属性的定义。

//---------------------------------------------------------------------------

// Resource Information

//---------------------------------------------------------------------------

// Toolbox bitmap resource IDs numbers.

//---------------------------------------------------------------------------

#define IDBMP_DROPFILEUP 8000

#define IDBMP_DROPFILEDOWN 8001

#define IDBMP_DROPFILEMONO 8003

#define IDBMP_DROPFILEEGA 8006

#define VBX_COMPANYNAME "Zeal SoftStudio.\0"

#define VBX_FILEDESCRIPTION "Visual Basic DropFile Custom Control\0"

#define VBX_INTERNALNAME "DropFile\0"

#define VBX_LEGALCOPYRIGHT "Copyright \251 Hai Li, Zeal SoftStudio. 1996\0"

#define VBX_LEGALTRADEMARKS "Zeal\231 is a trademark of Zeal SoftStudio.\0"

#define VBX_ORIGINALFILENAME "DROPFILE.VBX\0"

#define VBX_PRODUCTNAME "Zeal\231 VBXSuit\231 for Windows\231\0"

#define VBX_VERSION 1,00,0,00

#define VBX_VERSION_STR "1.00.000\0"

#ifndef RC_INVOKED

//---------------------------------------------------------------------------

// Macro for referencing member of structure

//---------------------------------------------------------------------------

#define OFFSETIN(struc, field) ((USHORT)&(((struc *)0)->field))

//---------------------------------------------------------------------------

// Control and Window Procedures

//---------------------------------------------------------------------------

LONG FAR PASCAL _export DropFileCtlProc(HCTL, HWND, USHORT, USHORT, LONG);

//---------------------------------------------------------------------------

// DROPFILE control data and structs

//---------------------------------------------------------------------------

typedef struct tagDROPFILE

{

HANDLE hDrop;

WORD nListCount;

BOOL bAccept;

char About[40];

} DROPFILE, FAR * LPDROPFILE;

//---------------------------------------------------------------------------

// Property info

//---------------------------------------------------------------------------

PROPINFO Property_About =

{

"(About)",

DT_HSZ | PF_fSetData | PF_fGetData,

OFFSETIN(DROPFILE, About), 0, 0, NULL, 0

};

PROPINFO Property_List =

{

"FileList",

DT_HSZ | PF_fPropArray| PF_fGetMsg | PF_fNoRuntimeW | PF_fNoShow,

0, 0, 0, NULL, 0

};

PROPINFO Property_ListCount =

{

"ListCount",

DT_SHORT | PF_fGetData | PF_fNoRuntimeW | PF_fNoShow,

OFFSETIN(DROPFILE, nListCount),

0, 0, NULL, 0

};

//---------------------------------------------------------------------------

// Property list

//---------------------------------------------------------------------------

// Define the consecutive indicies for the properties

//---------------------------------------------------------------------------

#define IPROP_DROPFILE_CTLNAME 0

#define IPROP_DROPFILE_ENABLED 6

#define IPROP_DROPFILE_ABOUT 10

#define IPROP_DROPFILE_LISTCOUNT 11

#define IPROP_DROPFILE_LIST 12

PPROPINFO DropFile_Properties[] =

{

PPROPINFO_STD_CTLNAME,

PPROPINFO_STD_TAG,

PPROPINFO_STD_LEFTNORUN,

PPROPINFO_STD_TOPNORUN,

PPROPINFO_STD_WIDTH,

PPROPINFO_STD_HEIGHT,

PPROPINFO_STD_ENABLED,

PPROPINFO_STD_PARENT,

PPROPINFO_STD_HELPCONTEXTID,

PPROPINFO_STD_ALIGN,

&Property_About,

&Property_ListCount,

&Property_List,

NULL

};

//---------------------------------------------------------------------------

// Event list

//---------------------------------------------------------------------------

// Define the consecutive indicies for the events

//---------------------------------------------------------------------------

#define IEVENT_DROPFILE_DROP 0

WORD Paramtypes_Drop[] = {ET_I2, ET_I2};

EVENTINFO Event_Drop =

{

"DropFiles",

2, 4 , Paramtypes_Drop,

"X As Integer, Y As Integer"

};

PEVENTINFO DropFile_Events[] =

{

&Event_Drop,

NULL

};

//---------------------------------------------------------------------------

// Model struct

//---------------------------------------------------------------------------

// Define the control model (using the event and property structures).

//---------------------------------------------------------------------------

MODEL modelDropFile =

{

VB_VERSION, // VB version being used

MODEL_fInvisAtRun, // MODEL flags

(PCTLPROC)DropFileCtlProc, // Control procedures

0, // Class style

0L, // Window style

sizeof(DROPFILE), // Size of DROPFILE structure

IDBMP_DROPFILEUP, // Palette bitmap ID

"DropFile", // Default control name

"DropFile", // Visual Basic class name

NULL, // Parent class name

DropFile_Properties, // Property information table

DropFile_Events, // Event information table

-1 // Property representing value of ctl

};

LPMODEL modellistDropFile[] =

{

&modelDropFile,

NULL

};

MODELINFO modelInfoDropFile =

{

VB_VERSION, // VB version being used

modellistDropFile // MODEL list

};

#endif // RC_INVOKED

在这里有几个结构需要引起注意。

用于说明属性的结构是PROPINFO,它是这样定义的:

typedef struct tagPROPINFO

{

PSTR npszName; // 属性名

FLONG fl; // 标志位

BYTE offsetData; // 数据储存在结构中的偏移量,设置了

// PF_fSetData或PF_fGetData标志后有效

BYTE infoData; // 0_INFO

LONG dataDefault; // 用于压缩存取

PSTR npszEnumList; // 如果数据为枚举型,用于枚举列表

BYTE enumMax; // 枚举型的最大值

} PROPINFO;

在这个结构中,标志位是最重要的,它规定了属性的数据类型、存储方式和响应方式。这里仅列出本文使用的几种标志位的含义,其余的请参阅VB的CDK手册或VBAPI.HLP。

标志 含义

DT_HSZ 该属性为字符串型

DT_SHORT 该属性为短整型

PF_fGetData 直接从offsetData指定的位置读取数据

PF_fGetMsg 当程序读取该属性时,产生VBM_GETPROPERTY消息

PF_fNoRuntimeW 该属性在运行时只能读,不能写

PF_fNoShow 该属性不出现在属性窗口中

PF_fPropArray 数组属性,必须和 PF_fNoShow一起使用

PF_fSetData 直接向offsetData指定的位置存储数据

PF_fSetMsg 当程序设置该属性时,产生VBM_SETPROPERTY消息

用于声明事件的结构是EVENTINFO,它是这样定义的:

typedef struct tagEVENTINFO

{

PSTR npszName; // 事件过程名

USHORT cParms; // 参数个数(不包括控制数组的Index参数)

USHORT cwParms; // 参数长度(为参数个数的2倍)

PWORD npParmTypes; // 参数类型

PSTR npszParmProf; // 参数串(将出现在VB的代码窗内)

FLONG fl; // 标志位

} EVENTINFO;

我们的DropFile事件包括两个参数,均为Integer型(即ET_I2),X表示释放时的横坐标,Y表示纵坐标。参数类型npParmTypes指向一个参数列表,其排列按照在VB中出现的顺序。

注册事件和属性部分的工作是在VBInitCCVBGetModelInfo中完成的,其代码如下:

// 全局变量

HINSTANCE hmodDLL;

#ifndef RUNTIME

WORD cVbxUsers = 0;

BOOL fDevTimeInited = FALSE;

BOOL fDlgInUse = FALSE;

#endif

BOOL FAR PASCAL _export VBInitCC

(

USHORT usVersion, BOOL bRuntime

)

{

#ifndef RUNTIME

// Count the number of hosts using this VBX. A host can be vb.exe,

// any .exe compiled from vb which uses this custom control, or any

// other program which loads and uses VBX files.

++cVbxUsers;

// Register popup class if this is from the development environment.

if(!bRuntime && !fDevTimeInited) {

WNDCLASS wndclass;

wndclass.style = 0;

wndclass.lpfnWndProc = (WNDPROC)AboutPopupWndProc;

wndclass.cbClsExtra = 0;

wndclass.cbWndExtra = 0;

wndclass.hInstance = hmodDLL;

wndclass.hIcon = NULL;

wndclass.hCursor = NULL;

wndclass.hbrBackground = NULL;

wndclass.lpszMenuName = NULL;

wndclass.lpszClassName = "AboutDummy";

if(!RegisterClass(&wndclass)) return FALSE;

// We successfully initialized the stuff we need at dev time

fDevTimeInited = TRUE;

}

#else

if(!bRuntime) return FALSE;

#endif

return VBRegisterModel(hmodDLL, &modelDropFile);

}

VOID FAR PASCAL _export VBTermCC(VOID)

{

#ifndef RUNTIME

--cVbxUsers;

if(cVbxUsers == 0 && fDevTimeInited) {

// Free any resources created for Dev environment

UnregisterClass("AboutDummy", hmodDLL);

}

#endif

}

LPMODELINFO FAR PASCAL _export VBGetModelInfo

(

USHORT usVersion

)

{

return &modelInfoDropFile;

}

二、控制过程的设计

控制过程的基本设计思想同窗口过程(WndProc)类似。控制过程一般要处理以下消息:

消息 用途

WM_CREATE 控制创建时发送,一般用来对属性初始化。

VBM_CHECKPROPERTY 设置属性时发送(在 VBM_SETPROPERTY之前),一般用来检查数据是否合法。

VBM_GETPROPERTY 读取属性时发送,控制过程应返回属性值。

VBM_SETPROPERTY 设置属性时发送,控制过程应保存属性值。

VBM_METHOD 调用方法时发送。

VBM_INITPROPPOPUP 在设计(design)状态设置属性时发送,一般用来提供特别的输入界面(详见“排他式对话框的设计”)。

WM_DESTROY 控制卸载时发送,一般用来释放资源。

文件拖放控制的控制过程如下:

// Far pointer to the default procedure.

FARPROC lpfnOldProc = (FARPROC) NULL ;

// Get the controls parent handle(form1).

HWND hParent ;

//---------------------------------------------------------------------------

// Standard Error Values

//---------------------------------------------------------------------------

#define ERR_DropFile_NONE 0

#define ERR_DropFile_OUTOFMEM 7

#define ERR_DropFile_SUBSCRIPT 9

#define ERR_DropFile_BADINDEX 381

// Error$(381) = "Invalid property array index"

//---------------------------------------------------------------------------

// Other Constants

//---------------------------------------------------------------------------

#define USE_DEFAULT_PROC -1

LONG FAR PASCAL _export DropFileCtlProc

(

HCTL hctl, HWND hwnd,

USHORT msg, USHORT wp, LONG lp

)

{

LONG lRetVal;

BOOL bEnabled = -1;

LPDROPFILE lpDropFile;

lpDropFile = (LPDROPFILE)VBDerefControl(hctl);

switch(msg) {

case WM_CREATE:

#ifndef RUNTIME

VBSetControlProperty(hctl, IPROP_DROPFILE_ABOUT,

(LONG)(LPSTR)"Click on \"...\" for About Box -->");

#endif

VBSetControlProperty(hctl, IPROP_DROPFILE_ENABLED,

(LONG)(LPVOID)&bEnabled);

switch (VBGetMode()){

// This will only be processed during run mode.

case MODE_RUN:

hParent = GetParent (hwnd) ;

// Get the address instance to normal proc.

lpfnOldProc = (FARPROC) GetWindowLong

(hParent, GWL_WNDPROC) ;

// Reset the address instance to the new proc.

SetWindowLong (hParent, GWL_WNDPROC,

(LONG) SbClsProc);

if(SetProp(hParent, "DropFileHwnd",

(HANDLE)hwnd)==NULL)

return ERR_DropFile_OUTOFMEM;

}

break;

case VBM_GETPROPERTY:// Get the property value.

lRetVal = DoGetProperty(hctl, wp, (LPDATASTRUCT)lp);

if(lRetVal != USE_DEFAULT_PROC) return lRetVal;

break;

case VBM_SETPROPERTY:// Set the property value.

lRetVal = DoSetProperty(hwnd, wp, (LPDATASTRUCT)lp);

if(lRetVal != USE_DEFAULT_PROC) return lRetVal;

break;

case WM_DESTROY:

// Deallocate all memory associated with the control.

if(lpDropFile->hDrop) DragFinish(lpDropFile->hDrop);

RemoveProp(GetParent(hwnd), "DropFileHwnd");

break;

#ifndef RUNTIME

case VBM_INITPROPPOPUP:

switch(wp) {

case IPROP_DROPFILE_ABOUT:

// Show About dialog.

if(fDlgInUse)

// Our dialog is already in use, so return NULL here

// to avoid bringing up a 2nd instance of the dialog.

return NULL;

return HwndInitAboutPopup();

}

break;

#endif

}

return VBDefControlProc(hctl, hwnd, msg, wp, lp);

}

只有对那些使用了PF_fGetMsg和PF_fSetMsg标志的属性进行读写操作时,VB才会发送相应的消息。消息的短参数(wp)是属性的索引号,这个索引号就是该属性在DropFile_Properties 数组中的下标,从0开始。VB在编译和运行时都是根据这个索引号来确定属性的,如果搞错了就会出现“张冠李戴”现象。VBX在版本升级时应保证索引号不变,否则就不能保证同以前版本兼容。VB的标准属性基本上都是采用消息方式的,这为我们提供了一些方便。比如,我们借助VB的Enabled属性来决定窗口是否接收文件释放,在用户设置Enabled属性时,VB发出VBM_GETPROPERTY消息,此时控制过程根据属性值来调用DragAcceptFile函数。

按VB手册的讲法,VBM_GETPROPERTYVBM_SETPROPERTY事件的长参数(lp)是一个DATASTRUCT结构的指针。该结构的定义如下:

typedef struct tagDATASTRUCT

{

LONG data; // 数据值

USHORT cindex; // 索引个数

struct

{

USHORT datatype; // 只能是DT_SHORT

LONG data; // 属性数组的下标

} index[1];

} DATASTRUCT;

实际上,只有当属性是数组型的时候,lp才是指向这一数据结构的指针。对于其他类型而言,lp只是一个该属性值数据的指针,此时如果使用lpds->data获得数据,一定会引起GPF错误。希望这能引起VBX设计的初学者的注意。

属性读写处理的部分很简单,仅列于此不做进一步的解释了。

//---------------------------------------------------------------------------

// This routine retrieves the value of the property setting.

//---------------------------------------------------------------------------

LONG NEAR PASCAL DoGetProperty

(

HCTL hctl, USHORT iprop, LPDATASTRUCT lpds

)

{

LONG lRetVal;

switch(iprop) {

case IPROP_DROPFILE_LIST:

lRetVal = GetPropertyArray(hctl, iprop, lpds);

if(lRetVal != USE_DEFAULT_PROC) return lRetVal;

break;

}

return USE_DEFAULT_PROC;

}

LONG NEAR PASCAL GetPropertyArray

(

HCTL hctl, USHORT iprop, LPDATASTRUCT lpds

)

{

LPDROPFILE lpDropFile;

SHORT nIndex;

char buffer[MAXPATH];

lpDropFile = (LPDROPFILE)VBDerefControl(hctl);

nIndex = (SHORT)lpds->index[0].data;

if(nIndex<0 || nIndex >= lpDropFile->nListCount)

return ERR_DropFile_SUBSCRIPT;

switch(iprop){

case IPROP_DROPFILE_LIST:

DragQueryFile(lpDropFile->hDrop, nIndex, buffer, MAXPATH);

lpds->data = (LONG)VBCreateHsz(FP_SEG(hctl), (LPSTR)buffer);

return ERR_DropFile_NONE;

}

return USE_DEFAULT_PROC;

}

//---------------------------------------------------------------------------

// This routine saves the value of the property setting.

//---------------------------------------------------------------------------

LONG NEAR PASCAL DoSetProperty(

HWND hwnd, USHORT iprop, LPDATASTRUCT lpds

)

{

switch(iprop) {

case IPROP_DROPFILE_ENABLED:

if(VBGetMode() != MODE_DESIGN)

DragAcceptFiles(GetParent(hwnd), (SHORT)lpds);

}

return USE_DEFAULT_PROC;

}

三、产生自定义事件

作为拖放控制的一个重要部分,就是产生自定义的DropFile事件。何时产生DropFile事件决定于何时得到WM_DROPFILES消息。控制过程可以得到各种消息,但是WM_DROPFILES消息不会传给控制过程,因为接受释放文件的窗口是拖放控制的父窗口,而不是拖放控制本身。为了获取WM_DROPFILES消息,我们采用了一种被称为“Subclass”的技术,也就是用我们编写的过程替换VB窗口的默认过程,在我们处理了WM_DROPFILES消息后,再将消息传递给VB窗口的默认过程。具体的操作见前面控制过程中对WM_CREATE消息处理的部分。

在我们的Subclass过程中,一旦发生WM_DROPFILES消息,就利用VB的FireEvent函数产生自定义事件。

typedef struct tagDROPPARMS

{

int far *Y; // 第二个参数

int far *X; // 第一个参数

LPVOID index; // 控制数组的索引,由VB填入

} DROPPARMS;

LONG FAR PASCAL _export SbClsProc (

HWND hwnd, USHORT msg, USHORT wp, LONG lp

)

{

switch (msg){

case WM_DROPFILES:{

HWND hwndctl = (HWND)GetProp(hwnd, "DropFileHwnd");

HCTL hctl;

if(hwndctl){

DROPPARMS params;

POINT pt;

LPDROPFILE lpDropFile;

DragQueryPoint(wp, &pt);

params.X = &pt.x;

params.Y = &pt.y;

hctl = VBGetHwndControl(hwndctl);

lpDropFile = (LPDROPFILE)VBDerefControl(hctl);

if(lpDropFile->hDrop) DragFinish(lpDropFile->hDrop);

lpDropFile->hDrop = (HANDLE)wp;

lpDropFile->nListCount =

DragQueryFile((HANDLE)wp, -1, NULL, 0);

VBFireEvent(hctl,

IEVENT_DROPFILE_DROP, &params);

}

}

break;

case WM_DESTROY:

SetWindowLong (hwnd, GWL_WNDPROC,

(LONG) lpfnOldProc);

break ;

}

// Call OldProc to process any other messages.

return (CallWindowProc(lpfnOldProc, hwnd, msg, wp, lp));

}

自定义事件的参数是利用DROPPARMS结构传递给VB的,这个结构中各参数的顺序同VB中定义的顺序正好相反,这点要特别注意。由于VB默认的参数传递方式是传参(by reference, 或称传址),所以该结构中的各项都应为指针型变量。

四、排他式对话框的设计

现在,VBX的设计者们也日益注意对用户友好的问题,一个突出的表现就是在VB的设计环境中提供各种直观的对话框帮助程序员设置各种属性。通常这些对话框是在用鼠标单击属性窗口的“...”按钮后弹出的(图2),它们的一个共同点就是均为排他式(modal,或称模态)对话框,即在关闭对话框之前,不能进行对话框以外的操作。在VBX中设计这种对话框需要一些特别的技巧。我们的拖放控制的属性比较少,所以我们增加了一个“(About)”属性来演示这种对话框的设计。

插图

要实现这种对话框,首先要处理VBM_INITPROPPOPUP消息,这个消息是在VB属性窗口中显示属性值之前发送,如果VBX提供对话框支持,则返回给VB一个窗口句柄(HWND)。VB在用户单击了“...”按钮后,激活这一窗口。但是,如果简单地将对话框的句柄传递给VB的话,这个窗口不能实现排他式操作,用户有可能因此出现误操作。我们采用的策略是:先建立一个“哑窗口”,并把这个窗口的句柄传给VB,当VB激活这一窗口时,我们并不显示这个窗口,而借用它作为父窗口调用VBDialogBox函数显示真正的对话框,这样就可以实现排他式操作了。“哑窗口”定义部分在VBInitCC函数中已经给出了,这里给出显示部分:

//---------------------------------------------------------------------------

// Create our property popup-window. Since we want to put up a dialog, this

// window never becomes visible. Instead, when asked to become visible, it

// will post a message to itself, remining it to put up our dialog.

// NOTE: May return NULL!

//---------------------------------------------------------------------------

#ifndef RUNTIME

HWND NEAR HwndInitAboutPopup( VOID)

{

return CreateWindow("AboutDummy", NULL, WS_POPUP,

0, 0, 0, 0, NULL, NULL, hmodDLL, NULL);

}

//---------------------------------------------------------------------------

// Open the (About) property dialog.

//---------------------------------------------------------------------------

LRESULT CALLBACK _export AboutPopupWndProc

(

HWND hwnd, USHORT msg,

USHORT wp, LONG lp

)

{

switch(msg) {

case WM_DESTROY:

fDlgInUse = FALSE;

break;

case WM_SHOWWINDOW:

if(wp) {

VBDialogBoxParam(hmodDLL, "AboutBox", AboutBoxProc, 0L);

return 0L;

}

}

return DefWindowProc(hwnd, msg, wp, lp);

}

//---------------------------------------------------------------------------

// The Dialog Procedure for the (About) property dialog.

//---------------------------------------------------------------------------

BOOL FAR PASCAL _export AboutBoxProc

(

HWND hDlg,

USHORT msg,

USHORT wp,

LONG lp

)

{

switch(msg) {

case WM_INITDIALOG: {

RECT rect;

int nx, ny; // New x and y

int width, height;

// Position dialog so it looks nice:

GetWindowRect(hDlg, &rect);

width = rect.right - rect.left;

height = rect.bottom - rect.top;

nx = (GetSystemMetrics(SM_CXSCREEN) - width) / 2;

ny = (GetSystemMetrics(SM_CYSCREEN) - height) / 3;

MoveWindow(hDlg, nx, ny, width, height, FALSE);

return TRUE;

}

case WM_COMMAND:

switch(wp) {

case IDOK:

case IDCANCEL:

EndDialog(hDlg, TRUE);

return TRUE;

}

}

return FALSE;

}

//---------------------------------------------------------------------------

// This routine is called when the first client loads the DLL.

//---------------------------------------------------------------------------

BOOL FAR PASCAL LibMain

(

HINSTANCE hmod, WORD wDataSeg,

WORD cbHeapSize, LPSTR lpszCmdLine

)

{

hmodDLL = hmod;

return TRUE;

}

#endif

你可能已经注意到了,在本文的程序中有大量的#ifndef RUNTIME ... #endif语句,这些语句大多同About属性有关。About属性只用于VB的设计环境,对EXE文件毫无用处,所以如果你在编译程序前定义了RUNTIME常量,这些语句就不会出现在最后的VBX文件里,这种VBX被称为“运行时间版本”(Runtime Version)。你可以在设计VB程序时使用完整版本,而在最后交付用户时提供运行时间版本,这样可以减少文件的大小,更重要的是可以避免非法用户使用你的VBX,保护你的权益。

“麻雀虽小,五脏俱全”。我们的这个拖放控制虽然简单,却包括了VBX设计的方方面面,希望它能启发你设计更好的控制。限于篇幅,我们略去了资源文件(.RC)、模块定义文件(.DEF)和VB的示范程序,对程序也做了适当地简化。

回到《李海文选》目录

如果您有任何建议,请给我发电子邮件:
版权所有 李海,热情软件屋 1997-2006


WU Banner from WebUnion Chinese Network