Jay Allen、Mark Davis、Heidi Housten, 和 Dan Mohr
Microsoft Corporation
各位,本月对于 Web Team 来说非同寻常。好消息是,Microsoft 的引人注目的新操作系统 Windows XP 终于上市开始零售了!自发布 RC1 (候选版) 以来,我们的公司和家庭就一直在使用 XP 系统,但我们并不强烈推荐读者使用,因为其增强的 UI 和杰出的即插即用功能仅仅是证明了更新程度而已。
坏消息是(除非您居住在亚利桑那州或纽约),西雅图水手队赢得优胜锦标赛的希望落空了。令人非常吃惊的是,我们小组的一位成员 — 就是不像他的五个孩子,仍然保持默默无闻的那位 — 克服种种困难,开始了英美文学课程学习的新生之旅。因此,这使得本月的文章标题有点华而不实。我们希望,如果您没有从中获取到所需信息,也能够从中得到一些乐趣。如果有人能够说出本文标题出自 20 世纪哪一首经典诗歌,就可以获得奖励。(相信您不会耍花招 — 和大家的想法不同,我们确实不知道最近您是否使用过 MSN Search。)
幸运地是,水手队的失败使我们可以重新将精力集中到定期安排好的小把戏上来,即创建 DHTML 等待屏幕、利用 ASP.NET Web 控件和数据绑定提升开发速度以及利用少许神奇的C++ 向 Internet Explorer 添加弹出式菜单(不过希望您不要介意悄然出现在上一个示例中的 Sodo Mojo)。
如有任何疑问,请随时 将您的问题告知我们。
池中漫游 | |
神奇的信号灯 | |
得心应手 | |
Web Team 简短问答 |
亲爱的 Web Team:
我想按照我在其他 Web 页中的所见来完成一些操作:(1) 将表单或请求提交至 Web 服务器;(2) 该 Web 服务器发送回带有消息 "please wait while the request is being processed...this may take several seconds" 的 Web 页;(3) 该服务器执行一些处理操作;并 (4) 返回最终结果。我自己在执行上述操作时没有成功,尽管我预计这是一个简单的解决方案。任何建议都会对我有所帮助。
致谢,
Brian Grobbel
Web Team 的答复:
有关软件性能的一句谚语说得很明白,即“欲速则不达”。然而,对于我们之中需要使用网页进行操作的人而言,我们需要分析其他方案。可利用的最佳技术之一是异步客户端/服务器通讯。该技术通过在代码等待来自服务器的数据的同时,允许用户与浏览器交互,以提高认知绩效。脚本中的常规事件处理程序运行在单线程上,只要在该脚本代码中进行同步调用,该线程就会阻塞 Internet Explorer UI 工作。要避免此种情况,可以使用在之前的“Web Team 访谈”专栏中讨论过的 window.setTimeout。对于客户端/服务器通讯,您有许多方案,其中包括下载行为、XMLHttpRequest 对象以及新的 webService 行为。这些方案全部都可用于异步数据检索。现在,如果我们想以异步方式进行数据检索以便 UI 保持响应,但是同时避免用户执行任何影响代码状态的进一步操作,那情况会怎样呢?据我看,示例一切正常!
<HTML> <HEAD> <TITLE>Fake Modal Dialog Sample</TITLE> </HEAD> <BODY STYLE="FONT-SIZE: 10pt; FONT-FAMILY: Verdana, Arial, Helvetica"> <SCRIPT LANGUAGE="JScript"> var NUMBER_OF_REPETITIONS = 40; var nRepetitions = 0; var g_oTimer = null; function startLongProcess() { divProgressDialog.style.display = ""; resizeModal(); btnCancel.focus(); // Add a resize handler for the window window.onresize = resizeModal; // Add a warning in case anyone tries to navigate away or refresh the page window.onbeforeunload = showWarning; // // Here's where you would normally kick off a long asynchronous process // like a file download or a remote database operation. Here, we use // our "long process" to simulate this process. // continueLongProcess(); } function updateProgress(nNewPercent) { // Update our pseudo progress bar divProgressInner.style.width = (parseInt(divProgressOuter.style.width) * nNewPercent / 100)+ "px"; } function stopLongProcess() { if (g_oTimer != null) { // Clear the timer so we don't get called back an extra time window.clearTimeout(g_oTimer); g_oTimer = null; } // Hide the fake modal DIV divModal.style.width = "0px"; divModal.style.height = "0px"; divProgressDialog.style.display = "none"; // Remove our event handlers window.onresize = null; window.onbeforeunload = null; nRepetitions = 0; } function continueLongProcess() { if (nRepetitions < NUMBER_OF_REPETITIONS) { // Set the timeout somewhere between 0 and .25 seconds var nTimeoutLength = Math.random() * 250; updateProgress(100 * nRepetitions / NUMBER_OF_REPETITIONS); g_oTimer = window.setTimeout("continueLongProcess();", nTimeoutLength); nRepetitions++; } else { stopLongProcess(); } } function showWarning() { //Warn users before they refresh the page or navigate away return "Navigating to a different page or refreshing the window could cause you to lose precious data.\n\nAre you *absolutely* certain you want to do this?"; } function resizeModal() { // Resize the DIV which fakes the modality of the dialog DIV divModal.style.width = document.body.scrollWidth; divModal.style.height = document.body.scrollHeight; // Re-center the dialog DIV divProgressDialog.style.left = ((document.body.offsetWidth - divProgressDialog.offsetWidth) / 2); divProgressDialog.style.top = ((document.body.offsetHeight - divProgressDialog.offsetHeight) / 2); } </SCRIPT> <INPUT TYPE="BUTTON" VALUE="Click Me!" onclick="startLongProcess();"> <!-- BEGIN PROGRESS DIALOG --> <DIV STYLE="BORDER: buttonhighlight 2px outset; FONT-SIZE: 8pt; Z-INDEX: 4; FONT-FAMILY: Tahoma; POSITION: absolute; BACKGROUND-COLOR: buttonface; DISPLAY: none; WIDTH: 350px; CURSOR: default" ID="divProgressDialog" onselectstart="window.event.returnValue=false;"> <DIV STYLE="PADDING: 3px; FONT-WEIGHT: bolder; COLOR: captiontext; BORDER-BOTTOM: white 2px groove; BACKGROUND-COLOR: activecaption"> Downloading Requested Document </DIV> <DIV STYLE="PADDING: 5px"> Please wait while I download the document you requested. </DIV> <DIV STYLE="PADDING: 5px"> This may take several seconds. </DIV> <DIV STYLE="PADDING: 5px"> <DIV ID="divProgressOuter" STYLE="BORDER: 1px solid threedshadow; WIDTH: 336px; HEIGHT: 15px"> <DIV ID="divProgressInner" STYLE="COLOR: white; TEXT-ALIGN: center; BACKGROUND-COLOR: infobackground; MARGIN: 0px; WIDTH: 0px; HEIGHT: 13px;"></DIV> </DIV> </DIV> <DIV STYLE="BORDER-TOP: white 2px groove; PADDING-BOTTOM: 5px; PADDING-TOP: 3px; BACKGROUND-COLOR: buttonface; TEXT-ALIGN: center"> <INPUT STYLE="FONT-FAMILY: Tahoma; FONT-SIZE: 8pt" TYPE="button" ID="btnCancel" onclick="stopLongProcess();" VALUE="Cancel"> </DIV> </DIV> <!-- END PROGRESS DIALOG --> <!-- BEGIN FAKE MODAL DIV--> <DIV ID="divModal" STYLE="BACKGROUND-COLOR: white; FILTER: alpha(opacity=75); LEFT: 0px; POSITION: absolute; TOP: 0px; Z-INDEX: 3" onclick="window.event.cancelBubble=true; window.event.returnValue=false;"> </DIV> <!-- END FAKE MODAL DIV --> </HTML>
该代码使用一些很棒的 DHTML 诀窍(例如较大的半透明 DIV),以在其进行运转的同时,基本上禁用与该页内容的交互。伪对话框使用户可以估计当前进度,并允许用户在无法忍受等待时取消操作。其他两个注意事项是,如果 UI 包含选择框,则需要在执行操作时将这些选择框的显示属性设置为 无,因为它们可以在 DIV 的顶部上方进行绘制(有关详细信息,请参阅知识库文章 Q177378)。而且,您不能阻止用户刷新页面或从某页导航离开(两种情况都会严重影响耗时较长的操作)。您可以向用户呈现一个弹出式对话框,以在他们采取这些操作之前提出警告。使用动态 onbeforeunload 处理程序可以实现此目的。
亲爱的 Web Team:
我想为 IE 的自定义工具栏按钮(如 Mail 按钮)添加一个弹出式菜单。我知道这是一个可以实现的操作,但我不确定该从何处着手。您能给予我帮助吗?
致谢,
Tom
Web Team 的答复:
在我们深入讨论这个问题之前,如果您还没有熟悉基本知识,那么希望您参阅 MSDN 中的文档“Adding Toolbar Buttons”以及“Toolbar Button Style Guide”。在这样一个秋高气爽的日子里,还有什么比通过棒球来演示向工具栏按钮添加弹出式菜单更好的方法呢?让我们假设水手队没有在美国冠军联赛中败给杨克队(真的希望这不是假设),并且我们希望随时单击一下或两下就可以得到自己喜欢的球员的相关资料。出于这个目的,我们将创建一个带有弹出式菜单的工具栏按钮,该菜单上满是球员的名字。单击这些名字会将浏览器导航至正确的统计数据页。(我们知道可以将这些链接添加到自己的收藏菜单,但是这将需要三次单击,这会使我们成为大忙人!)
首先,在 Visual C++ 中创建一个新的 ATL DLL 项目。然后,添加一个带有双接口的 ATL 对象,该对象(使用默认的实现 IObjectWithSiteImpl)实现 IObjectWithSite。接下来,添加另一个接口 IOleCommandTarget(我们必须自己来实现这个)。这个接口对于支持用户交互是必需的;没有它,我们只能说是空有华丽外表而已。此时,还需要编辑 ATL 用于注册组件的 .rgs 文件,以遵循上述指导。还应将按钮的图标作为资源进行添加。现在,开始进入有趣的部分。我们将重写 IObjectWithSite::SetSite 的默认实现,并在我们的版本内存储一个指向顶层浏览器窗口的指针,稍后我们需要它来进行导航。
下一步是充分实现 IOleCommandTarget 接口的方法。幸运地是,此处只有两个方法,即 QueryStatus和 Exec。Internet Explorer 工具栏使用 QueryStatus 方法来询问按钮的当前状态。我们的实现将只需回复支持和启用所有的命令即可。主要的重担落在了 Exec 上,Exec 在用户实际单击按钮时激发。在实现中,我们将执行以下操作:
• |
使用 Win32 API 创建弹出式菜单和子菜单。 |
• |
通过工具栏按钮获取光标位置,以确定我们的索引。 |
• |
使用特殊的工具栏控件消息来获取更多有关按钮的信息。 |
• |
暂时让按钮呈现按下状态(如 mail 按钮)。 |
• |
显示我们的弹出式菜单并跟踪用户的选择。 |
• |
释放按钮。 |
• |
按照用户的选择,将浏览器导航至适当的球员页。 |
以下是我们的按钮实现中主要的代码片段:
STDMETHODIMP CButtonObj::SetSite(IUnknown* pUnkSite) { // If pUnkSite is not NULL, a new site is being set. if (pUnkSite) { IServiceProvider* pISP = NULL; if (m_pWB) { m_pWB->Release(); m_pWB = NULL; } if (SUCCEEDED(pUnkSite->QueryInterface(IID_IServiceProvider, (void **)&pISP))) { if (FAILED(pISP->QueryService(IID_IWebBrowserApp, IID_IWebBrowser2, (void **) &m_pWB))) { // Just in case m_pWB = NULL; } pISP->Release(); pISP = NULL; } } else { m_pWB->Release(); m_pWB = NULL; } return S_OK; } STDMETHODIMP CButtonObj::QueryStatus( const GUID* pguidCmdGroup, ULONG cCmds, OLECMD* prgCmds, OLECMDTEXT* pCmdText ) { HRESULT hr = OLECMDERR_E_UNKNOWNGROUP; if (pguidCmdGroup && IsEqualGUID(*pguidCmdGroup, CLSID_ToolbarExtButtons)) { for (ULONG i = 0; i < cCmds; i++) { // By default, we'll support all commands prgCmds[i].cmdf = OLECMDF_ENABLED | OLECMDF_SUPPORTED; // If we wanted to latch the button down, we could do this: // prgCmds[i].cmdf |= OLECMDF_LATCHED; } hr = S_OK; } return hr; } STDMETHODIMP CButtonObj::Exec( const GUID* pguidCmdGroup, DWORD nCmdID, DWORD nCmdexecopt, VARIANTARG* pvaIn, VARIANTARG* pvaOut ) { HRESULT hr = S_OK; if (pguidCmdGroup == NULL) { switch (nCmdID) { case 0: // Called from toolbar button { HMENU hMenu, hSubMenuPitchers, hSubMenuFielders; HWND hWndToolbar; WCHAR wszURL[512]; LRESULT lResult; TBBUTTONINFO tbbi; POINT pt; RECT rect = {0,0,0,0}; long nOpt = 0; long nButtonCount = 0; int nTargetButton = 0; int i = 0; tbbi.cbSize = sizeof(tbbi); tbbi.dwMask = TBIF_COMMAND | TBIF_BYINDEX; // Create our popup menus/submenus hMenu = CreatePopupMenu(); hSubMenuPitchers = CreatePopupMenu(); AppendMenu(hSubMenuPitchers, MF_STRING, 119469, "Jamie Moyer"); AppendMenu(hSubMenuPitchers, MF_STRING, 119704, "Jeff Nelson"); AppendMenu(hSubMenuPitchers, MF_STRING, 121125, "Arthur Rhodes"); AppendMenu(hSubMenuPitchers, MF_STRING, 277408, "Kazuhiro Sasaki"); AppendMenu(hSubMenuPitchers, MF_STRING, 121986, "Aaron Sele"); AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hSubMenuPitchers, "Pitchers"); hSubMenuFielders = CreatePopupMenu(); AppendMenu(hSubMenuFielders, MF_STRING, 110816, "David Bell"); AppendMenu(hSubMenuFielders, MF_STRING, 111214, "Bret Boone"); AppendMenu(hSubMenuFielders, MF_STRING, 111904, "Mike Cameron"); AppendMenu(hSubMenuFielders, MF_STRING, 136722, "Carlos Guillen"); AppendMenu(hSubMenuFielders, MF_STRING, 118808, "Mark McLemore"); AppendMenu(hSubMenuFielders, MF_STRING, 119976, "John Olerud"); AppendMenu(hSubMenuFielders, MF_STRING, 400085, "Ichiro Suzuki"); AppendMenu(hSubMenuFielders, MF_STRING, 124383, "Dan Wilson"); AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hSubMenuFielders, "Fielders"); GetCursorPos(&pt); hWndToolbar = WindowFromPoint(pt); nButtonCount = (long)SendMessage(hWndToolbar, TB_BUTTONCOUNT, 0, 0); ScreenToClient(hWndToolbar, &pt); for (i=0; i < nButtonCount; i++) { // We're going to try to figure out which button we are // by seeing which one the cursor is inside lResult = SendMessage(hWndToolbar, TB_GETITEMRECT, (WPARAM)i, (LPARAM)&rect ); if (PtInRect(&rect, pt)) { // Found it nTargetButton = i; // Break out of the loop i = nButtonCount; } } // Get the button command index SendMessage(hWndToolbar, TB_GETBUTTONINFO, (WPARAM)nTargetButton, (LPARAM)&tbbi); // Make the button appear pressed SendMessage(hWndToolbar, TB_CHECKBUTTON, (WPARAM)tbbi.idCommand, (LPARAM)TRUE); // Translate our coordinates so the menu shows up in the right place pt.x = rect.left; pt.y = rect.bottom; ClientToScreen(hWndToolbar, &pt); // Get the user selection nOpt = TrackPopupMenu(hMenu, TPM_RETURNCMD, pt.x, pt.y, 0, hWndToolbar, NULL); DestroyMenu(hSubMenuPitchers); DestroyMenu(hSubMenuFielders); DestroyMenu(hMenu); // Make the button not appear pressed SendMessage(hWndToolbar, TB_CHECKBUTTON, (WPARAM)tbbi.idCommand, (LPARAM)FALSE); if (nOpt != 0 && m_pWB != NULL) { // Navigate the browser! VARIANT vEmpty; vEmpty.vt = VT_EMPTY; VARIANT vURL; swprintf(wszURL, L"http://www.myfakestatssite.com/player.asp?id=%d", nOpt); vURL.vt = VT_BSTR; vURL.bstrVal = SysAllocString(wszURL); m_pWB->Navigate2(&vURL, &vEmpty, &vEmpty, &vEmpty, &vEmpty); SysFreeString(vURL.bstrVal); } break; } default: hr = OLECMDERR_E_NOTSUPPORTED; break; } } else { hr = OLECMDERR_E_UNKNOWNGROUP; } return hr; }
亲爱的 Web Team:
我喜欢 ASP.NET DataGrid Web 控件是如此易于上手,但是我想让用户通过单击任意列来选择行,而不是如许多示例中所示,需具备特定的选择列。
Nigel
Web Team 的答复:
使用 Microsoft ASP.NET 和 Microsoft Visual Studio_ .NET 的最大的好处之一是,您可以快速开发一个 Web 应用程序,以使用 ASP 和 DHTML 来执行需要编写许多行代码的任务。使用 ASP.NET Web 窗体和控件来开发下一个 Web 应用程序,将使您能够不用过多考虑服务器/客户端、跨浏览器以及语言特定的性质的 Web 开发,并集中注意力在如何做到最好,即为您的客户提供最棒的功能。当然,当所有事情对于您来说都变得比较简单之后,有时这些优点或许并不能始终为您提供所需的解决方案。这时,您会希望对 ASP.NET Web 窗体和控件一探究竟。
DataGrid 是一个 ASP.NET Web 控件,可用于 Web 窗体上。该 DataGrid 提供了作为标准 HTML 表格输出的数据视图。您只需指定该数据源和控件样式即可。可以指定要显示的列和希望支持的命令,例如选择、排序以及编辑。可以用短短几行代码来实现此目的。(注:runat="server" 表示此为服务器端控件。)如果要使用 Visual Studio .NET,只需将 DataGrid 控件拖放到 Web 窗体即可。以下是 DataGrid 出现在 Web 窗体 (.ASPX) 文件中时,有关其的一个简单声明:
<form id="Form1" method="post" runat="server"> <asp:DataGrid ID="mygrid" Runat="server" /> </form>
除由 Visual Studio .NET 提供的代码外,还有以下支持 Web 窗体的 C# 代码中要求的代码,以显示每个人喜欢的数据库的标题表格。
using System.Data.SqlClient; protected DataGrid mygrid; private void Page_Load(object sender, System.EventArgs e) { if ( ! IsPostBack ) LoadGrid(); } private void LoadGrid() { SqlConnection sc = new SqlConnection("server=(local);database=pubs; Integrated Security=SSPI"); DataSet ds = new DataSet(); SqlDataAdapter da = new SqlDataAdapter( "", sc ); da.SelectCommand.CommandText = "select * from titles"; // Retrieve the data set da.Fill( ds ); // Specify the data source mygrid.DataSource = ds; // Bind the data to the grid mygrid.DataBind(); }
提供行选择就像指定支持选择命令的按钮列一样简单:
<form id="Form1" method="post" runat="server"> <asp:DataGrid ID="mygrid" Runat="server"> <SelectedItemStyle BackColor="Cornsilk" /> <Columns> <asp:ButtonColumn CommandName="Select" Text="Select"/> </Columns> </asp:DataGrid> </form>
请注意,AutoGenerateColumns 属性默认情况下为真,因此,除选定列外,还有一个适用于每个数据集字段的列。用户可以单击第一列以选定行。SelectedItemStyle 标记可以让我们指定选定行的样式。
为了允许用户通过单击任何列来选择行,您可以在自己的 ASPX 文件中提供客户端脚本,只要单击表格,就会调用该脚本:
<!-- Hook up grid events --> <script language="jscript" for="mygrid" event="onclick"> selectRow( "mygrid" ); </script> <script language=jscript> function selectRow( name ) { // Get row that was clicked var oRow = window.event.srcElement.parentElement; // Get rows in table var oRows = oRow.parentElement.parentElement.rows; if ( oRows ) { var nRows = oRows.length-1; // Find the clicked row for ( var i=1; i<nRows; i++ ) { if ( oRows[i] == oRow ) { // Found - post an event to the server __doPostBack( name + ":_ctl" + (i+1) + ":_ctl0", "" ); return; } } } return; } </script>
客户端脚本通过将第一列的单击事件返回服务器,找到单击的行,并模拟行的选择。现在,您可以将 ButtonColumn 的 Visible 属性设置为假,以隐藏该列。请注意,当您隐藏上述示例中的选择列时,__doPostBack 函数不可用,因为该页上没有其他控件。
我已经编写了可运行在 Microsoft Internet Explorer 4.0 或更高版本上的示例脚本。您可以构建一个利用 ASP.NET Request 对象提供的 HttpBrowserCapabilities 属性的 ASP.NET 用户控件,以确定所需的客户端脚本。或者,可以重写该脚本以运行在多个浏览器上。
何时更改
问:Mark Davison 想在提交表单之前辨别其上的值是否已从默认值进行了更改。
答:Mark,您想要的神奇的属性是 defaultValue。为您的元素设置 onchange 事件处理程序,并使用 window.event.srcElement 从该事件处理程序内获取对激发元素的引用。如果您决定提交用户的新输入,可以将该输入指派为 defaultValue 以使其成为新的初始值。
function change() { var currElem = window.event.srcElement; window.alert(currElem.defaultValue); // Perform validation on new value here. Var isValid = PerformValidation(currElem.value); if (isValid) { currElem.defaultValue = currElem.value; } else { currElem.value = currElem.defaultValue; } }
的确存在时间
问:Rani Gopakumar 希望知道如何使用 ASP 和 VBScript 在客户端显示当前系统时间。
答:要返回与服务器相关的日期和时间,请使用 Now 函数。您可以使用 FormatDateTime 函数将时间部分提取出来:
<% Response.Write "System time: " & FormatDateTime(Now(), vbLongTime) & "<br>" %>
如同手术台上麻醉了的病人
问: G. Aguilar 想知道为什么他不能让浮动表格出现在 SELECT 元素之上。
答:知识库文章 Q177378 向读者讲明了这一点。.SELECT 元素是有窗口的控件,而有窗口的控件始终出现在无窗口控件的上方,并且,截至 Internet Explorer 5.5,Document Object Model 中 SELECT 旁的所有其他对象都是无窗口控件。唯一的解决方法是,使用 DHTML 行为、ATL Full 控件或 Windowless 属性设为真的 Visual Basic UserControl 来创建您自己的无窗口 SELECT 对象。
我将告诉您所有一切
问: Alan Canning 希望知道如何在已具备主题和正文的客户端上生成电子邮件。
答:通过初始化 la a URL QueryString 的 Subject 和 Body 变量来使用 mailto:协议。然后,将您对 document.location 对象创建的 URL 进行指派:
<SCRIPT> function mail() { var mailUrl = "mailto:[email protected]?subject=Hello, world&Body=This is my message to the Web Team."; document.location = mailUrl; } </SCRIPT> <BUTTON onclick="mail();"> Prepare mail message </BUTTON>
嗨,不要问“这是什么?”
问:David Taylor 希望知道为什么 IIS5 下的 BrowserType 对象不能正确检测 Internet Explorer 6.0。
答:BrowserType 使用 browscap.ini 文件作为它的 浏览器数据库。您需要使用以下 User-Agent 字符串,对自己的 browscap.ini(位于 %SYSTEMDIR%\inetsrv)进行更新以包括 Internet Explorer 6:
[Mozilla/4.0 (compatible; MSIE 6.*; Windows 95*)] [Mozilla/4.0 (compatible; MSIE 6.*; Windows 98*)] [Mozilla/4.0 (compatible; MSIE 6.*; Windows NT*)] [Mozilla/4.0 (compatible; MSIE 6.*)]
您可以从 Windows XP Professional 获得 browscap.ini 的更新版本,在您读到《前进,Windows 小组!》这篇文章或不想使用 Windows Server 2003(保持协调)时,该操作系统将已经上市。遗憾地是,Windows XP Pro 版本具有一个小错误。Internet Explorer 6 的 beta 属性在应当为 false 时却被设为了 true。请在闲暇时将这个疏漏进行更正。
一个令人无法抗拒的问题
问:Tom Glover 和 Kurt Petrucci 希望知道如何从客户端脚本打开“打印预览”对话框。
答:我们没有找到可直接进行此操作的好办法。您的最佳选择将是 ActiveX_ 控件,该控件检索其主机帧的 IWebBrowser2 接口(请参阅知识库文章 Q172763),并利用 IDM_PRINTPREVIEW 的命令标识符调用其 ExecWB() 方法。或者,您可以将 WebBrowser 控件宿主在 HTML 页内,并根据该宿主调用 ExecWB()(虽然知识库文章 Q183048 会告诉您如何做,但是我们出于各种理由不建议这样做)。
Web Team
Mark Davis 是 Internet Explorer SDK 小组的一名软件设计工程师。Mark 来自英格兰,目前正在进行攀登西北部主要顶峰的训练。
Heidi Housten是瑞典 Microsoft Consulting Services 的一名咨询师,这之前曾在 Developer Support 和 MSDN 工作过一段时间。有人说她是为了逃避西雅图的细雨才来到瑞典,这只是一个谣言。其实她来这是为了参加 8 月份的传统小龙虾聚会。
Dan Mohr 是 Microsoft Developer Support 的 Internet Client 小组的一名工程师,业余时间都花在了在地下室录制乐队音乐、编写自己的 Commodore 64 程序以及赞美 70 年代末期的朋克摇滚的优点上。
Jay Allen,Microsoft Developer Support 的 Internet Client 的一名支持工程师,他渴望将记事本和 Emacs Lisp 集成。他和四个孩子呆在一起的时间真的很少,通常他都将时间花在了阅读数学书籍、学习日语和Haskell 编程上面。