文件拖放(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的开发一般是使用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,其中包括了事件和属性的定义。
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Toolbox bitmap resource IDs numbers.
//---------------------------------------------------------------------------
#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_STR "1.00.000\0"
//---------------------------------------------------------------------------
// 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
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
DT_HSZ | PF_fSetData | PF_fGetData,
OFFSETIN(DROPFILE, About), 0, 0, NULL, 0
DT_HSZ | PF_fPropArray| PF_fGetMsg | PF_fNoRuntimeW | PF_fNoShow,
DT_SHORT | PF_fGetData | PF_fNoRuntimeW | PF_fNoShow,
OFFSETIN(DROPFILE, nListCount),
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// 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[] =
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Define the consecutive indicies for the events
//---------------------------------------------------------------------------
#define IEVENT_DROPFILE_DROP 0
WORD Paramtypes_Drop[] = {ET_I2, ET_I2};
PEVENTINFO DropFile_Events[] =
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Define the control model (using the event and property structures).
//---------------------------------------------------------------------------
VB_VERSION, // VB version being used
MODEL_fInvisAtRun, // MODEL flags
(PCTLPROC)DropFileCtlProc, // Control procedures
sizeof(DROPFILE), // Size of DROPFILE structure
IDBMP_DROPFILEUP, // Palette bitmap ID
"DropFile", // Default control name
"DropFile", // Visual Basic class name
DropFile_Properties, // Property information table
DropFile_Events, // Event information table
-1 // Property representing value of ctl
VB_VERSION, // VB version being used
modellistDropFile // MODEL list
BYTE offsetData; // 数据储存在结构中的偏移量,设置了
// PF_fSetData或PF_fGetData标志后有效
PSTR npszEnumList; // 如果数据为枚举型,用于枚举列表
在这个结构中,标志位是最重要的,它规定了属性的数据类型、存储方式和响应方式。这里仅列出本文使用的几种标志位的含义,其余的请参阅VB的CDK手册或VBAPI.HLP。
PF_fGetData 直接从offsetData指定的位置读取数据
PF_fGetMsg 当程序读取该属性时,产生VBM_GETPROPERTY消息
PF_fPropArray 数组属性,必须和 PF_fNoShow一起使用
PF_fSetData 直接向offsetData指定的位置存储数据
PF_fSetMsg 当程序设置该属性时,产生VBM_SETPROPERTY消息
USHORT cParms; // 参数个数(不包括控制数组的Index参数)
USHORT cwParms; // 参数长度(为参数个数的2倍)
PSTR npszParmProf; // 参数串(将出现在VB的代码窗内)
我们的DropFile事件包括两个参数,均为Integer型(即ET_I2),X表示释放时的横坐标,Y表示纵坐标。参数类型npParmTypes指向一个参数列表,其排列按照在VB中出现的顺序。
注册事件和属性部分的工作是在VBInitCC和VBGetModelInfo中完成的,其代码如下:
BOOL FAR PASCAL _export VBInitCC
USHORT usVersion, BOOL bRuntime
// 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.
// Register popup class if this is from the development environment.
if(!bRuntime && !fDevTimeInited) {
wndclass.lpfnWndProc = (WNDPROC)AboutPopupWndProc;
wndclass.hbrBackground = NULL;
wndclass.lpszClassName = "AboutDummy";
if(!RegisterClass(&wndclass)) return FALSE;
// We successfully initialized the stuff we need at dev time
return VBRegisterModel(hmodDLL, &modelDropFile);
VOID FAR PASCAL _export VBTermCC(VOID)
if(cVbxUsers == 0 && fDevTimeInited) {
// Free any resources created for Dev environment
UnregisterClass("AboutDummy", hmodDLL);
LPMODELINFO FAR PASCAL _export VBGetModelInfo
控制过程的基本设计思想同窗口过程(WndProc)类似。控制过程一般要处理以下消息:
VBM_CHECKPROPERTY 设置属性时发送(在 VBM_SETPROPERTY之前),一般用来检查数据是否合法。
VBM_GETPROPERTY 读取属性时发送,控制过程应返回属性值。
VBM_SETPROPERTY 设置属性时发送,控制过程应保存属性值。
VBM_INITPROPPOPUP 在设计(design)状态设置属性时发送,一般用来提供特别的输入界面(详见“排他式对话框的设计”)。
// Far pointer to the default procedure.
FARPROC lpfnOldProc = (FARPROC) NULL ;
// Get the controls parent handle(form1).
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
#define ERR_DropFile_OUTOFMEM 7
#define ERR_DropFile_SUBSCRIPT 9
#define ERR_DropFile_BADINDEX 381
// Error$(381) = "Invalid property array index"
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
LONG FAR PASCAL _export DropFileCtlProc
USHORT msg, USHORT wp, LONG lp
lpDropFile = (LPDROPFILE)VBDerefControl(hctl);
VBSetControlProperty(hctl, IPROP_DROPFILE_ABOUT,
(LONG)(LPSTR)"Click on \"...\" for About Box -->");
VBSetControlProperty(hctl, IPROP_DROPFILE_ENABLED,
// This will only be processed during run mode.
// Get the address instance to normal proc.
lpfnOldProc = (FARPROC) GetWindowLong
// Reset the address instance to the new proc.
SetWindowLong (hParent, GWL_WNDPROC,
if(SetProp(hParent, "DropFileHwnd",
case VBM_GETPROPERTY:// Get the property value.
lRetVal = DoGetProperty(hctl, wp, (LPDATASTRUCT)lp);
if(lRetVal != USE_DEFAULT_PROC) return lRetVal;
case VBM_SETPROPERTY:// Set the property value.
lRetVal = DoSetProperty(hwnd, wp, (LPDATASTRUCT)lp);
if(lRetVal != USE_DEFAULT_PROC) return lRetVal;
// Deallocate all memory associated with the control.
if(lpDropFile->hDrop) DragFinish(lpDropFile->hDrop);
RemoveProp(GetParent(hwnd), "DropFileHwnd");
// Our dialog is already in use, so return NULL here
// to avoid bringing up a 2nd instance of the dialog.
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_GETPROPERTY和VBM_SETPROPERTY事件的长参数(lp)是一个DATASTRUCT结构的指针。该结构的定义如下:
USHORT datatype; // 只能是DT_SHORT
实际上,只有当属性是数组型的时候,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
lRetVal = GetPropertyArray(hctl, iprop, lpds);
if(lRetVal != USE_DEFAULT_PROC) return lRetVal;
LONG NEAR PASCAL GetPropertyArray
HCTL hctl, USHORT iprop, LPDATASTRUCT lpds
lpDropFile = (LPDROPFILE)VBDerefControl(hctl);
nIndex = (SHORT)lpds->index[0].data;
if(nIndex<0 || nIndex >= lpDropFile->nListCount)
return ERR_DropFile_SUBSCRIPT;
DragQueryFile(lpDropFile->hDrop, nIndex, buffer, MAXPATH);
lpds->data = (LONG)VBCreateHsz(FP_SEG(hctl), (LPSTR)buffer);
//---------------------------------------------------------------------------
// This routine saves the value of the property setting.
//---------------------------------------------------------------------------
LONG NEAR PASCAL DoSetProperty(
HWND hwnd, USHORT iprop, LPDATASTRUCT lpds
if(VBGetMode() != MODE_DESIGN)
DragAcceptFiles(GetParent(hwnd), (SHORT)lpds);
作为拖放控制的一个重要部分,就是产生自定义的DropFile事件。何时产生DropFile事件决定于何时得到WM_DROPFILES消息。控制过程可以得到各种消息,但是WM_DROPFILES消息不会传给控制过程,因为接受释放文件的窗口是拖放控制的父窗口,而不是拖放控制本身。为了获取WM_DROPFILES消息,我们采用了一种被称为“Subclass”的技术,也就是用我们编写的过程替换VB窗口的默认过程,在我们处理了WM_DROPFILES消息后,再将消息传递给VB窗口的默认过程。具体的操作见前面控制过程中对WM_CREATE消息处理的部分。
在我们的Subclass过程中,一旦发生WM_DROPFILES消息,就利用VB的FireEvent函数产生自定义事件。
LPVOID index; // 控制数组的索引,由VB填入
LONG FAR PASCAL _export SbClsProc (
HWND hwnd, USHORT msg, USHORT wp, LONG lp
HWND hwndctl = (HWND)GetProp(hwnd, "DropFileHwnd");
hctl = VBGetHwndControl(hwndctl);
lpDropFile = (LPDROPFILE)VBDerefControl(hctl);
if(lpDropFile->hDrop) DragFinish(lpDropFile->hDrop);
lpDropFile->hDrop = (HANDLE)wp;
DragQueryFile((HANDLE)wp, -1, NULL, 0);
IEVENT_DROPFILE_DROP, ¶ms);
SetWindowLong (hwnd, GWL_WNDPROC,
// 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.
//---------------------------------------------------------------------------
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
VBDialogBoxParam(hmodDLL, "AboutBox", AboutBoxProc, 0L);
return DefWindowProc(hwnd, msg, wp, lp);
//---------------------------------------------------------------------------
// The Dialog Procedure for the (About) property dialog.
//---------------------------------------------------------------------------
BOOL FAR PASCAL _export AboutBoxProc
// Position dialog so it looks nice:
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);
//---------------------------------------------------------------------------
// This routine is called when the first client loads the DLL.
//---------------------------------------------------------------------------
HINSTANCE hmod, WORD wDataSeg,
WORD cbHeapSize, LPSTR lpszCmdLine
你可能已经注意到了,在本文的程序中有大量的#ifndef RUNTIME ... #endif语句,这些语句大多同About属性有关。About属性只用于VB的设计环境,对EXE文件毫无用处,所以如果你在编译程序前定义了RUNTIME常量,这些语句就不会出现在最后的VBX文件里,这种VBX被称为“运行时间版本”(Runtime Version)。你可以在设计VB程序时使用完整版本,而在最后交付用户时提供运行时间版本,这样可以减少文件的大小,更重要的是可以避免非法用户使用你的VBX,保护你的权益。
“麻雀虽小,五脏俱全”。我们的这个拖放控制虽然简单,却包括了VBX设计的方方面面,希望它能启发你设计更好的控制。限于篇幅,我们略去了资源文件(.RC)、模块定义文件(.DEF)和VB的示范程序,对程序也做了适当地简化。
如果您有任何建议,请给我发电子邮件:
。
版权所有 李海,热情软件屋 1997-2006