教學課程:建立裝載 WPF 內容的 Win32 應用程式
更新:2007 年 11 月
若要將 WPF 放入 Win32 應用程式內,請使用 HwndSource (其提供內含 WPF 內容的 HWND)。首先請建立 HwndSource,並為其指定類似 CreateWindow 的參數。接著,請告知 HwndSource 您希望它包含的 WPF 內容。最後,請從 HwndSource 取得 HWND。此逐步解說在說明如何在 Win32 應用程式內建立混合的 WPF,以實作作業系統的 [日期和時間內容] 對話方塊。
必要條件
請參閱 WPF 和 Win32 互通性概觀。
如何使用本教學課程
本教學課程的重點為產生互通性應用程式的重要步驟。課程中以 Win32 時鐘互通性範例做為教學輔助,不過這個範例反映的是最終的成果。本教學課程記載步驟的方式就像您以自己現有的 Win32 專案 (可能是預先存在的專案) 開始作業,並將裝載 WPF 加入您的應用程式中。您可以比較自己的最終成果與 Win32 時鐘互通性範例。
Win32 內的 Windows Presentation Framework 逐步解說 (HwndSource)
下圖顯示本教學課程預定產生的最終成果:
您可以在 Microsoft Visual Studio 中建立 C++ Win32 專案,並使用對話方塊編輯器建立下列項目,以便重新建立這個對話方塊:
(您不需要使用 Microsoft Visual Studio 來使用 HwndSource,也不需要使用 C++ 來撰寫 Win32 程式,但這是執行這項工作相當典型的方式,而且可為其本身提供逐步的教學說明)。
您必須完成五個特殊的子步驟,才能將 WPF 時鐘放入對話方塊中。
透過在 Microsoft Visual Studio 中變更專案設定,使您的 Win32 專案能夠呼叫 Managed 程式碼 (/clr)。
在個別的 DLL 中建立 WPFPage。
將該 WPFPage 放入 HwndSource 內。
使用 Win32,決定在較大的 Win32 應用程式內放置 HWND 的位置。
/clr
第一個步驟是要將這個 Unmanaged Win32 專案變成可以呼叫 Managed 程式碼的專案。請使用 /clr 編譯器 (Compiler) 選項,以連結到您想使用的必要 DLL,並調整 Main 方法以搭配 WPF 使用。
若要啟用在 C++ 專案中使用 Managed 程式碼的功能,請以滑鼠右鍵按一下 win32clock 專案,然後選取 [屬性]。接著在 [一般] 屬性頁 (預設值) 上,將 Common Language Runtime 支援變更為 /clr。
接下來,加入 WPF 所需 DLL 的參考:PresentationCore.dll、PresentationFramework.dll、System.dll、WindowsBase.dll、UIAutomationProvider.dll 和 UIAutomationTypes.dll (下列指示假設作業系統安裝在 C: 磁碟機上)。
以滑鼠右鍵按一下 win32clock 專案,並選取 [參考],然後在該對話方塊內:
以滑鼠右鍵按一下 win32clock,並選取 [參考]。
按一下 [加入新參考],再按一下 [瀏覽] 索引標籤,輸入 C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll,然後按 [確定]。
對 PresentationFramework.dll 重複相同的動作:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll。
對 WindowsBase.dll 重複相同的動作:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll。
對 UIAutomationTypes.dll 重複相同的動作:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationTypes.dll。
對 UIAutomationProvider.dll 重複相同的動作:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationProvider.dll。
按一下 [加入新參考],選取 System.dll,然後按一下 [確定]。
按一下 [確定],結束用來加入參考的 [win32clock 屬性頁]。
最後,將 STAThreadAttribute 加入 _tWinMain 方法中,以搭配 WPF 使用:
[System::STAThreadAttribute]
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
這個屬性會告知 Common Language Runtime (CLR),當它初始化元件物件模型 (COM) 時,應該使用 WPF (和 Windows Form) 所需的單一執行緒 Apartment Model (STA)。
建立 Windows Presentation Framework 頁面
接著,請建立定義 WPFPage 的 DLL。最簡單的方法通常是建立 WPFPage 做為獨立的應用程式,然後再以這種方式撰寫並偵錯 WPF 的部分。完成之後,可以使用滑鼠右鍵按一下該專案,再按一下 [屬性],移到 [應用程式],然後將 [輸出類型] 改成 [Windows 類別庫],將專案轉換成 DLL。
接著 WPF dll 專案可以和 Win32 專案合併 (一個方案包含兩個專案) – 以滑鼠右鍵按一下方案,並選取 [加入現有專案]。
若要從 Win32 專案使用該 WPF dll,您必須加入參考。
以滑鼠右鍵按一下 win32clock,並選取 [參考]。
按一下 [加入新參考]。
按一下 [專案] 索引標籤。選取 WPFClock,再按一下 [確定]。
按一下 [確定],結束用來加入參考的 [win32clock 屬性頁]。
HwndSource
接著,請使用 HwndSource,讓 WPFPage 的外觀變成與 HWND 類似。請將下列程式碼區塊加入至 C++ 檔案:
namespace ManagedCode
{
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Windows::Media;
HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
HwndSource^ source = gcnew HwndSource(
0, // class style
WS_VISIBLE | WS_CHILD, // style
0, // exstyle
x, y, width, height,
"hi", // NAME
IntPtr(parent) // parent window
);
UIElement^ page = gcnew WPFClock::Clock();
source->RootVisual = page;
return (HWND) source->Handle.ToPointer();
}
}
}
這段程式碼較長,可以使用部分說明。第一個部分是各種不同的子句,因此您不需要完整限定所有的呼叫:
namespace ManagedCode
{
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Windows::Media;
接著請定義建立 WPF 內容、在其周圍放置 HwndSource 並傳回 HWND 的函式:
HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
首先請建立 HwndSource,其參數與 CreateWindow 類似:
HwndSource^ source = gcnew HwndSource(
0, // class style
WS_VISIBLE | WS_CHILD, // style
0, // exstyle
x, y, width, height,
"hi", // NAME
IntPtr(parent) // parent window
);
接著,請透過呼叫其建構函式的方式,建立 WPF 內容類別:
UIElement^ page = gcnew WPFClock::Clock();
接著將頁面連接到 HwndSource:
source->RootVisual = page;
在最後一行,傳回 HwndSource 的 HWND:
return (HWND) source->Handle.ToPointer();
定位 Hwnd
現在您已經有一個包含 WPF 時鐘的 HWND,接下來您必須將該 HWND 放入 Win32 對話方塊中。如果您知道放置 HWND 的正確位置,可直接將大小和位置傳遞至您稍早定義的 GetHwnd 函式。但是您是使用資源檔來定義對話方塊,所以無法確定任何 HWND 的正確位置。您可以使用 Microsoft Visual Studio 對話方塊編輯器,將 Win32 STATIC 控制項放在要顯示時鐘的位置 (「在此插入時鐘」),並使用該控制項來定位 WPF 時鐘。
處理 WM_INITDIALOG 時,您會使用 GetDlgItem 來擷取預留位置 STATIC 的 HWND:
HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);
接著再計算預留位置 STATIC 的大小和位置,使您可以將 WPF 時鐘放在該位置:
RECT rectangle;
GetWindowRect(placeholder, &rectangle);
int width = rectangle.right - rectangle.left;
int height = rectangle.bottom - rectangle.top;
POINT point;
point.x = rectangle.left;
point.y = rectangle.top;
result = MapWindowPoints(NULL, hDlg, &point, 1);
接著您會隱藏預留位置 STATIC:
ShowWindow(placeholder, SW_HIDE);
然後在該位置建立 WPF 時鐘 HWND:
HWND clock = ManagedCode::GetHwnd(hDlg, point.x, point.y, width, height);
為了讓教學課程更為有趣,也為了產生真實的 WPF 時鐘,此時您必須建立 WPF 時鐘控制項。您大多可以在標記中執行這項作業,而程式碼後置 (Code-Behind) 中只需包含幾個事件處理常式 (Event Handler)。由於這個教學課程是關於互通性,而不是關於控制項設計,因此這裡提供了 WPF 時鐘的完整程式碼做為程式碼區塊,而沒有建置該段程式碼的指示以及各部分所代表意思。請依需要使用此程式碼自行學習如何變更控制項的外觀、操作及功能。
標記如下:
<Page x:Class="WPFClock.Clock"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#fcfcfe" Offset="0" />
<GradientStop Color="#f6f4f0" Offset="1.0" />
</LinearGradientBrush>
</Grid.Background>
<Grid Name="PodClock" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.Resources>
<Storyboard x:Key="sb">
<DoubleAnimation From="0" To="360" Duration="12:00:00" RepeatBehavior="Forever"
Storyboard.TargetName="HourHand"
Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)"
/>
<DoubleAnimation From="0" To="360" Duration="01:00:00" RepeatBehavior="Forever"
Storyboard.TargetName="MinuteHand"
Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)"
/>
<DoubleAnimation From="0" To="360" Duration="0:1:00" RepeatBehavior="Forever"
Storyboard.TargetName="SecondHand"
Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)"
/>
</Storyboard>
</Grid.Resources>
<Ellipse Width="108" Height="108" StrokeThickness="3">
<Ellipse.Stroke>
<LinearGradientBrush>
<GradientStop Color="LightBlue" Offset="0" />
<GradientStop Color="DarkBlue" Offset="1" />
</LinearGradientBrush>
</Ellipse.Stroke>
</Ellipse>
<Ellipse VerticalAlignment="Center" HorizontalAlignment="Center" Width="104" Height="104" Fill="LightBlue" StrokeThickness="3">
<Ellipse.Stroke>
<LinearGradientBrush>
<GradientStop Color="DarkBlue" Offset="0" />
<GradientStop Color="LightBlue" Offset="1" />
</LinearGradientBrush>
</Ellipse.Stroke>
</Ellipse>
<Border BorderThickness="1" BorderBrush="Black" Background="White" Margin="20" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Name="MonthDay" Text="{Binding}"/>
</Border>
<Canvas Width="102" Height="102">
<Ellipse Width="8" Height="8" Fill="Black" Canvas.Top="46" Canvas.Left="46" />
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="0" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="30" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="60" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="90" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="120" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="150" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="180" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="210" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="240" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="270" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="300" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="330" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle x:Name="HourHand" Canvas.Top="21" Canvas.Left="48"
Fill="Black" Width="4" Height="30">
<Rectangle.RenderTransform>
<RotateTransform x:Name="HourHand2" CenterX="2" CenterY="30" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle x:Name="MinuteHand" Canvas.Top="6" Canvas.Left="49"
Fill="Black" Width="2" Height="45">
<Rectangle.RenderTransform>
<RotateTransform CenterX="1" CenterY="45" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle x:Name="SecondHand" Canvas.Top="4" Canvas.Left="49"
Fill="Red" Width="1" Height="47">
<Rectangle.RenderTransform>
<RotateTransform CenterX="0.5" CenterY="47" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
</Grid>
</Grid>
</Page>
隨附的程式碼後置則如下:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WPFClock
{
/// <summary>
/// Interaction logic for Clock.xaml
/// </summary>
public partial class Clock : Page
{
private DispatcherTimer _dayTimer;
public Clock()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Clock_Loaded);
}
void Clock_Loaded(object sender, RoutedEventArgs e) {
// set the datacontext to be today's date
DateTime now = DateTime.Now;
DataContext = now.Day.ToString();
// then set up a timer to fire at the start of tomorrow, so that we can update
// the datacontext
_dayTimer = new DispatcherTimer();
_dayTimer.Interval = new TimeSpan(1, 0, 0, 0) - now.TimeOfDay;
_dayTimer.Tick += new EventHandler(OnDayChange);
_dayTimer.Start();
// finally, seek the timeline, which assumes a beginning at midnight, to the appropriate
// offset
Storyboard sb = (Storyboard)PodClock.FindResource("sb");
sb.Begin(PodClock, HandoffBehavior.SnapshotAndReplace, true);
sb.Seek(PodClock, now.TimeOfDay, TimeSeekOrigin.BeginTime);
}
private void OnDayChange(object sender, EventArgs e)
{
// date has changed, update the datacontext to reflect today's date
DateTime now = DateTime.Now;
DataContext = now.Day.ToString();
_dayTimer.Interval = new TimeSpan(1, 0, 0, 0);
}
}
}
最終成果的外觀如下:
若要比較您的最終結果與產生此螢幕擷取畫面的程式碼,請參閱 Win32 時鐘互通性範例。