一种全新的软件界面设计方法(摘)

下载实例代码

关键字: COM MySpy IE SetUIHanlder IcustomDoc IDocHostUIHandler GetExternal

** 前言 **

作者在解决各种问题的时候喜欢首先使用 C++ Builder 来尝试,这篇文章也是这样,但这并不影响其他开发工具的使用者阅读,因为这都是微软的开发技术,选择什么工具并不重要,我们理解了他的原理可以使用任何工具实现同样的功能。

** 正文 **

使用过 VC.Net 的朋友可能知道,在 VC.Net 中全新提供了一种基于 Web 的界面设计方法,不过可能真正用到的人很少,至少我在国内的软件中没有看到过这样的界面设计方法。当初使用 VC.net 的时候就希望 BCB 的下个版本可以加入这样灵活的界面设计方法,但是到现在还没有等到,我想也不能一直这样等下去,于是就自己研究其中的实现方法,终于让我研究出来。这篇文章就是讨论这样方法,以及在软件设计设计中的可行性。

说了这么多,可能还有朋友不知道这样的界面到底有什么不同,有什么优点呢?如果你也有同样的好奇感的话,请你继续看下去。

在 Windows2000 下,大家经常使用控制面板 / 添加、卸载软件的对话框就是基于这样的界面 (Xp 下暂时不清楚),我不说出来可能很少有人知道-那个对话框整个就是个网页?什么你不相信?如果是网页为什么能和本地的计算机程序交互?为什么不能选择网页里面的文字?为什么不能弹出右键菜单?如果是网页,那它的 html 代码在那里?

为了证明上面的说法,我们需要一些特殊的软件,这个软件就是作者写的 MySpy ,可以到作者的站点 ( http://siney.yeah.net ) 免费下载使用,我们可以从 MySpy 的界面中看到添加 / 删除程序的对话框是个 Internet Explorer_Server ,这说明它是个网页,

在 MySpy 的 Web 页面还可以看到这个页面的地址是: res://sp3res.dll/default.hta,

近一步使用 MySpy 得到这个网页的代码(不能直接右键获取代码),部分如下:

  1<html xmlns:ctls=""><head><title id="ARP"> 添加  /  删除程序  </title>
  2
  3&lt; META http-equiv=Content-Type content="text/html; charset=gb2312"&gt;<base href="res://appwiz.cpl/"/><link href="arp.css" rel="stylesheet" type="text/css"/>
  4<style>>ctls\:PLACES { behavior: url(places.htc); }ctls\:LISTBOX { behavior: url(listbox.htc); }ctls\:ACCEL { behavior: url(accel.htc); }.PlacesBar {background-color:threedshadow}.Hide {display:none}.NonClientBackground { background-color: buttonface;}.Header { padding-bottom: 5px;vertical-align: text-top; }.GroupImage { margin-right: 5px;}.GroupDesc {padding-left: 1em;padding-right: 1em;}.AppNameRow {}.AppImageTD {width: 20px; padding: '4px 2px 2px 2px';}.InfoPane { padding-top:4px; vertical-align: top;}.PropLabel {width: 7em;padding-top: 2px;padding-bottom: 2px;padding-right: 3px;text-align: right;}.PropValue {width: 6em;text-align: right;padding-right: 7px;}.AddPropLabel {padding-top: 2px;padding-bottom: 2px;padding-right: 3px;text-align: right;}.AddPropValue {width: 13em;text-align: right;padding-right: 7px;}.ButtonDescPane { padding-top: 5px; padding-bottom: 7px;padding-right: 5px;}.ButtonPane { width: 15em; padding: 5px; text-align: right;}.FakeAnchor {cursor:hand;}#idClientCatName {font-weight: bold;padding-bottom: 1ex;}.disabled {color: graytext;}#idTblExtendedProps.Focus {color: highlighttext;}</style>  
  5  
  6---  
  7  
  8嗬嗬,是不是很神奇呢,这只是一个应用的例子,其实还有很多软件的界面使用了上面的方法来创建界面,比如  Norton AntiVirsu,MS Visual Studio.net  ,  C# Builder  等。其实深入仔细思考的话,这样的界面最困难的是如何和本地代码交互,为什么在网页里点一个按钮能执行自己的代码呢?有过  COM  编程经验的人,可能会想到用  COM  编写一个 **外部** 对象,在网页中使用脚本创建这个对象,然后调用对象的方法似乎可以完成这样的功能?但是这里有很多不好的地方: 
  9
 101\.  需要注册  COM  的本地运行安全,否则  IE  会有安全警告,这肯定是最终用户不愿意看到的; 
 11
 122\.  用户可以  轻松从  html  代码里获得  COM  对象的使用方法(就像上面用  MySpy  获得代码一样),这样他们可以轻松使用你的  COM  对象完成他们自己的界面,这样不够隐蔽,不安全。 
 13
 14也许还有更多不好的地方,但暂时作者没有想到,因为微软及其他软件公司都不是这样做的,他们也许知道更多。下面我们就来讨论一种既安全又隐蔽的实现方法。 
 15
 16从  IE4  开始,微软提供了一个 ** ICustomDoc  ** 接口, ** ICustomDoc  ** 的 ** SetUIHandler  ** 允许用户设置一个基于 ** IDocHostUIHandler  ** 的接口来接管界面处理器,在 ** IDocHostUIHandler  ** 提供了很多的虚拟方法,需要程序员来重载他们实现不同的定制功能,这里有一篇文章详细介绍了这些信息 _ http://msdn.microsoft.com/library/default.asp?url=/workshop/browser/hosting/wbcustomization.asp  , _ 在这里我们需要重载 ** GetExternal  ** 方法来扩展  IE DOM  ,如果我们成功的扩展了 ** DOM  ** ,那么我们就这可以这样编写  html  代码来实现与本地程序交互,例如: 
 17
 18<html>
 19<head>
 20<script language="JScript">
 21
 22function MyFunc() 
 23
 24{ 
 25
 26external.HelloWorld(); //HelloWorld  是我们扩展的方法 
 27
 28} 
 29
 30</script>
 31</head>
 32<body>
 33<input onclick="MyFunc();" type="Button" value="Show hello world"/>
 34</body>
 35</html>  
 36  
 37---  
 38  
 39** HelloWorld  ** 就是我们扩展的一个方法,当点击按钮的时候 ** external  ** 对象会调用 ** HelloWorld  ** 方法调用本地代码,对于 ** external  ** 对象则会调用上面提到的 ** GetExternal  ** 方法来查询是否提供了扩展,下面是如何实现 ** GetExternal  ** 方法来实现扩展 ** external  ** 对象,代码如下: 
 40
 41class MyDocHandler :public IDocHostUIHandler 
 42
 43{ 
 44
 45long refcount; 
 46
 47public: 
 48
 49MyDocHandler() :refcount(1){ } 
 50
 51virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID classid, void** intf) { 
 52
 53if (classid == IID_IUnknown) 
 54
 55{ 
 56
 57*intf = (IUnknown*)this; 
 58
 59AddRef(); 
 60
 61} 
 62
 63else if (classid == IID_IDocHostUIHandler) 
 64
 65{ 
 66
 67*intf = (IDocHostUIHandler*)this; 
 68
 69AddRef(); 
 70
 71} 
 72
 73else if (classid == IID_IDispatch) 
 74
 75{ 
 76
 77*intf = (IDispatch*)this; 
 78
 79AddRef(); 
 80
 81} 
 82
 83else 
 84
 85return E_NOINTERFACE; 
 86
 87return S_OK; 
 88
 89} 
 90
 91virtual ULONG STDMETHODCALLTYPE AddRef() { 
 92
 93InterlockedIncrement(&amp;refcount); 
 94
 95return refcount; 
 96
 97} 
 98
 99virtual ULONG STDMETHODCALLTYPE Release() { 
100
101InterlockedDecrement(&amp;refcount); 
102
103if (refcount == 0) 
104
105delete this; 
106
107return refcount; 
108
109} 
110
111//  返回  S_OK  ,屏蔽掉右键菜单 
112
113virtual HRESULT STDMETHODCALLTYPE ShowContextMenu( 
114
115/* [in] */ DWORD dwID, 
116
117/* [in] */ POINT __RPC_FAR *ppt, 
118
119/* [in] */ IUnknown __RPC_FAR *pcmdtReserved, 
120
121/* [in] */ IDispatch __RPC_FAR *pdispReserved) { 
122
123return S_OK; 
124
125} 
126
127virtual HRESULT STDMETHODCALLTYPE GetHostInfo( 
128
129/* [out][in] */ DOCHOSTUIINFO __RPC_FAR *pInfo) { 
130
131return E_NOTIMPL; 
132
133} 
134
135virtual HRESULT STDMETHODCALLTYPE ShowUI( 
136
137/* [in] */ DWORD dwID, 
138
139/* [in] */ IOleInPlaceActiveObject __RPC_FAR *pActiveObject, 
140
141/* [in] */ IOleCommandTarget __RPC_FAR *pCommandTarget, 
142
143/* [in] */ IOleInPlaceFrame __RPC_FAR *pFrame, 
144
145/* [in] */ IOleInPlaceUIWindow __RPC_FAR *pDoc) { 
146
147return E_NOTIMPL; 
148
149} 
150
151virtual HRESULT STDMETHODCALLTYPE HideUI( void) { 
152
153return E_NOTIMPL; 
154
155} 
156
157virtual HRESULT STDMETHODCALLTYPE UpdateUI( void) { 
158
159return E_NOTIMPL; 
160
161} 
162
163virtual HRESULT STDMETHODCALLTYPE EnableModeless( 
164
165/* [in] */ BOOL fEnable) { 
166
167return E_NOTIMPL; 
168
169} 
170
171virtual HRESULT STDMETHODCALLTYPE OnDocWindowActivate( 
172
173/* [in] */ BOOL fActivate) { 
174
175return E_NOTIMPL; 
176
177} 
178
179virtual HRESULT STDMETHODCALLTYPE OnFrameWindowActivate( 
180
181/* [in] */ BOOL fActivate) { 
182
183return E_NOTIMPL; 
184
185} 
186
187virtual HRESULT STDMETHODCALLTYPE ResizeBorder( 
188
189/* [in] */ LPCRECT prcBorder, 
190
191/* [in] */ IOleInPlaceUIWindow __RPC_FAR *pUIWindow, 
192
193/* [in] */ BOOL fRameWindow) { 
194
195return E_NOTIMPL; 
196
197} 
198
199virtual HRESULT STDMETHODCALLTYPE TranslateAccelerator( 
200
201/* [in] */ LPMSG lpMsg, 
202
203/* [in] */ const GUID __RPC_FAR *pguidCmdGroup, 
204
205/* [in] */ DWORD nCmdID) { 
206
207return E_NOTIMPL; 
208
209} 
210
211virtual HRESULT STDMETHODCALLTYPE GetOptionKeyPath( 
212
213/* [out] */ LPOLESTR __RPC_FAR *pchKey, 
214
215/* [in] */ DWORD dw) { 
216
217return E_NOTIMPL; 
218
219} 
220
221virtual HRESULT STDMETHODCALLTYPE GetDropTarget( 
222
223/* [in] */ IDropTarget __RPC_FAR *pDropTarget, 
224
225/* [out] */ IDropTarget __RPC_FAR *__RPC_FAR *ppDropTarget) { 
226
227return E_NOTIMPL; 
228
229} 
230
231virtual HRESULT STDMETHODCALLTYPE GetExternal( 
232
233/* [out] */ IDispatch __RPC_FAR *__RPC_FAR *ppDispatch) { 
234
235*ppDispatch = new MyCommandHandler(); 
236
237return S_OK; 
238
239} 
240
241virtual HRESULT STDMETHODCALLTYPE TranslateUrl( 
242
243/* [in] */ DWORD dwTranslate, 
244
245/* [in] */ OLECHAR __RPC_FAR *pchURLIn, 
246
247/* [out] */ OLECHAR __RPC_FAR *__RPC_FAR *ppchURLOut) { 
248
249return E_NOTIMPL; 
250
251} 
252
253virtual HRESULT STDMETHODCALLTYPE FilterDataObject( 
254
255/* [in] */ IDataObject __RPC_FAR *pDO, 
256
257/* [out] */ IDataObject __RPC_FAR *__RPC_FAR *ppDORet) { 
258
259return E_NOTIMPL; 
260
261} 
262
263};   
264  
265---  
266  
267上面重载了  ShowContextMenu  方法屏蔽掉右键菜单,使用户不能得到网页代码,关于  GetExternal  是这样实现的  : 
268
269virtual HRESULT STDMETHODCALLTYPE GetExternal( 
270
271/* [out] */ IDispatch __RPC_FAR *__RPC_FAR *ppDispatch) { 
272
273*ppDispatch = new MyCommandHandler(); 
274
275return S_OK; 
276
277}   
278  
279---  
280  
281可以看到只是简单返回了 ** MyCommandHandler  ** 对象, ** MyCommandHandler  ** 必须继承自 ** IDispatch  ** 接口来实现支持自动化的调用方式,它是这样实现的: 
282
283class MyCommandHandler : public IDispatch 
284
285{ 
286
287long refcount; 
288
289public: 
290
291MyCommandHandler() :refcount(1){ } 
292
293virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( 
294
295/* [out] */ UINT *pctinfo){ 
296
297return S_OK; 
298
299} 
300
301virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( 
302
303/* [in] */ UINT iTInfo, 
304
305/* [in] */ LCID lcid, 
306
307/* [out] */ ITypeInfo **ppTInfo){ 
308
309return S_OK; 
310
311} 
312
313virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( 
314
315/* [in] */ REFIID riid, 
316
317/* [size_is][in] */ LPOLESTR *rgszNames, 
318
319/* [in] */ UINT cNames, 
320
321/* [in] */ LCID lcid, 
322
323/* [size_is][out] */ DISPID *rgDispId){ 
324
325*rgDispId=1; 
326
327return S_OK; 
328
329} 
330
331virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
332
333/* [in] */ DISPID dispIdMember, 
334
335/* [in] */ REFIID riid, 
336
337/* [in] */ LCID lcid, 
338
339/* [in] */ WORD wFlags, 
340
341/* [out][in] */ DISPPARAMS *pDispParams, 
342
343/* [out] */ VARIANT *pVarResult, 
344
345/* [out] */ EXCEPINFO *pExcepInfo, 
346
347/* [out] */ UINT *puArgErr){ 
348
349if(dispIdMember==1) 
350
351{ 
352
353MessageBox(0,"Hello World","Hello",0); //place your code here 
354
355frmweb-&gt;Edit1-&gt;Text="Hello World(  这也可以?  )"; 
356
357} 
358
359return S_OK; 
360
361} 
362
363virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID classid, void** intf) { 
364
365if (classid == IID_IDispatch) 
366
367{ 
368
369*intf = (IDispatch*)this; 
370
371AddRef(); 
372
373} 
374
375else 
376
377return E_NOINTERFACE; 
378
379return S_OK; 
380
381} 
382
383virtual ULONG STDMETHODCALLTYPE AddRef() { 
384
385InterlockedIncrement(&amp;refcount); 
386
387return refcount; 
388
389} 
390
391virtual ULONG STDMETHODCALLTYPE Release() { 
392
393InterlockedDecrement(&amp;refcount); 
394
395if (refcount == 0) 
396
397delete this; 
398
399return refcount; 
400
401} 
402
403};   
404  
405---  
406  
407如果大家了解一些  COM  知识,我们知道这里关键的是 ** GetIDsOfNames  ** 和 ** Invoke  ** 方法的实现,因为自动化对象只能通过这样的方式来调用,而不能使用函数指针直接调用虚拟方法, ** GetIDsOfNames  ** 查询指定的函数名的调用  ID  ,就是说如果有一个方法是“  HelloWorld  ”,那么它会先调用 ** GetIDsOfNames  ** 方法来查询这个方法是否支持,如果支持则给出该方法的调用  ID  (通过修改  rgDispId[out]  参数),如果不支持则返回  E_NOTIMPL  ,他的实现简单如下: 
408
409virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( 
410
411/* [in] */ REFIID riid, 
412
413/* [size_is][in] */ LPOLESTR *rgszNames, 
414
415/* [in] */ UINT cNames, 
416
417/* [in] */ LCID lcid, 
418
419/* [size_is][out] */ DISPID *rgDispId){ 
420
421*rgDispId=1; 
422
423return S_OK; 
424
425}   
426  
427---  
428  
429这里我们假定了只有一个扩展函数  HelloWorld  ,如果有多个,我们需要比较  rgszNames  参数的函数名返回不同的调用  ID  ,有了调用  ID  ,实现  Invoke  方法就很简单了: 
430
431virtual HRESULT STDMETHODCALLTYPE Invoke( 
432
433/* [in] */ DISPID dispIdMember, 
434
435/* [in] */ REFIID riid, 
436
437/* [in] */ LCID lcid, 
438
439/* [in] */ WORD wFlags, 
440
441/* [out][in] */ DISPPARAMS *pDispParams, 
442
443/* [out] */ VARIANT *pVarResult, 
444
445/* [out] */ EXCEPINFO *pExcepInfo, 
446
447/* [out] */ UINT *puArgErr){ 
448
449if(dispIdMember==1) 
450
451{ 
452
453MessageBox(0,"Hello World","Hello",0); //place your code here 
454
455frmweb-&gt;Edit1-&gt;Text="Hello World(  这也可以?  )"; 
456
457} 
458
459return S_OK; 
460
461}   
462  
463---  
464  
465根据  dispIdMember  的不同实现不同的代码,如果方法是有参数的可以在  pDispParams  中取得,如果有什么不明白可以参考  MSDN  和一些  COM  的书籍,这里就不详细解释了。 
466
467最后我们要做的就是使 **我们的浏览器** 知道我们扩展了  external  ,代码如下: 
468
469dochandler = new MyDocHandler; 
470
471webBrowser-&gt;Navigate(WideString(L"E:\\\Projects\\\extWeb\\\ext.htm")); 
472
473while(webBrowser-&gt;Busy) 
474
475Application-&gt;ProcessMessages(); 
476
477ICustomDoc *custdoc; 
478
479webBrowser-&gt;Document-&gt;QueryInterface(&amp;custdoc); //  取得  IcustomDoc  接口 
480
481if (custdoc) 
482
483{ 
484
485custdoc-&gt;SetUIHandler(dochandler); //  设置我们自己的界面处理器 
486
487custdoc-&gt;Release(); 
488
489}   
490  
491---  
492  
493注意上面的粗体“ **我们的浏览器** ”,因为这样的  扩展仅  针对与自己程序里使用  WebBrowser  控件,不影响  IE  本身的扩展,也就是说那个  ext.htm  文件只能在我们的程序中有效,就算其他用户得到了这段  htm  代码也不能正常运行的,如果你想测试,你得到的是: 
494
495![](file:///E:/Web/article/newintf.files/image006.jpg) 因为他们并不知道如何扩展  external  对象,这点就解决了刚才我们说的使用  COM  的问题。 
496
497说句实话设计这样界面还是有一定难度的,那么它在实际开发中到底有什么好处呢?我想至少有以下几点: 
498
4991\.  界面设计和程序逻辑设计分离,美工可以和程序员一起工作,界面设计再也不是没有审美细胞程序员的问题; 
500
5012\.  轻松实现  Skin  功能,界面的改变不需要重新编译代码,只需要换一个不同  htm  代码文件就可以; 
502
5033\.  再也无法使用  Spy  工具获得窗体  Handler  做各种  Hook  ,使你的程序运行的更安全; 
504
5054\.  充分使用  IE  现有技术,搭建功能更强大的软件; 
506
5075\.  使你的软件看起来更酷,更专业。 
508
509怎么样?心动了吗?赶快改善你的界面吧。如果你有更多想法,可以通过  [email protected]  取得联系。</head></html>
Published At
Categories with Web编程
Tagged with
comments powered by Disqus