操作指南:在 Win32 中托管一个 WPF 时钟控件

若要将 WPF 放入 Win32 应用程序,请使用 HwndSource,它提供包含 WPF 内容的 HWND。 首先创建 HwndSource,为其提供类似于 CreateWindow 的参数。 然后,告知 HwndSource 你希望置于其中的 WPF 内容。 最后,从 HwndSource 中获取 HWND。 本演练演示如何在 Win32 应用程序中创建混合 WPF,以便重新实现操作系统 日期和时间属性 对话框。

先决条件

请参阅 WPF 和 Win32 互操作

如何使用本教程

本教程重点介绍生成互操作应用程序的重要步骤。 本教程由一个示例提供支持:Win32 时钟互操作示例,而该示例反映了最终产品。 本教程将介绍相关步骤,就好像从自己的现有 Win32 项目(可能是预先存在的项目)开始,并且正在向应用程序添加托管 WPF。 可以将最终产品与 Win32 时钟互操作示例进行比较。

Win32 中的 Windows Presentation Framework 的演练 (HwndSource)

下图显示了本教程的预期最终产品:

显示“日期和时间属性”对话框的屏幕截图。

可以通过在 Visual Studio 中创建 C++ Win32 项目并使用对话框编辑器创建以下内容来重新创建此对话:

“重新创建日期和时间属性”对话框

(无需使用 Visual Studio 来使用 HwndSource,并且无需使用C++编写 Win32 程序,但这是一种相当典型的方法,可以很好地进行分步教程说明)。

需要完成五个特定的子步骤,才能将 WPF 时钟放入对话框中:

  1. 通过更改 Visual Studio 中的项目设置,使 Win32 项目能够调用托管代码(/clr)。

  2. 在单独的 DLL 中创建 WPFPage

  3. 将 WPFPage 置于 HwndSource中。

  4. 使用 Page 属性获取该 Handle 的 HWND。

  5. 使用 Win32 确定在更大的 Win32 应用程序中放置 HWND 的位置

/clr

第一步是将此非托管 Win32 项目转换为可调用托管代码的项目。 使用 /clr 编译器选项,它将链接到要使用的所需 DLL,然后调整 Main 方法以便与 WPF 一起使用。

若要在C++项目中启用托管代码的使用:右键单击 win32clock 项目并选择 属性。 在 常规 属性页上(默认值),将公共语言运行时支持更改为 /clr

接下来,添加对 WPF 所需的 DLL 的引用:PresentationCore.dll、PresentationFramework.dll、System.dll、WindowsBase.dll、UIAutomationProvider.dll和 UIAutomationTypes.dll。 (以下说明假定操作系统安装在 C: 驱动器上。

  1. 右键单击 win32clock 项目,然后选择“引用...”,并在该对话框中

  2. 右键单击 win32clock 项目,然后选择“引用...”

  3. 单击 添加新引用,单击“浏览”选项卡,输入 C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll,然后单击“确定”。

  4. 对 PresentationFramework.dll 重复相同步骤:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll。

  5. 对 WindowsBase.dll 重复相同步骤:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll。

  6. 对 UIAutomationTypes.dll 重复相同步骤:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationTypes.dll。

  7. 对 UIAutomationProvider.dll 重复相同步骤:C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationProvider.dll。

  8. 单击 添加新引用,选择 System.dll,然后单击 “确定”

  9. 单击“确定”退出用于添加引用的 win32clock 属性页

最后,将 STAThreadAttribute 添加到用于 WPF 的 _tWinMain 方法中。

[System::STAThreadAttribute]
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)

此属性告诉公共语言运行时 (CLR),当它初始化组件对象模型 (COM) 时,它应使用单线程单元模型 (STA),这是 WPF(和 Windows 窗体)所必需的。

创建 Windows 演示文稿框架页

接下来,创建定义 WPFPage的 DLL。 通常最简单的方法是将 WPFPage 创建为独立应用程序,然后以这种方式编写和调试 WPF 部分。 完成后,可以通过右键单击项目、单击 属性、转到应用程序并将输出类型更改为 Windows 类库,将该项目转换为 DLL。

然后,WPF dll 项目可以与 Win32 项目(一个包含两个项目的解决方案)结合使用 – 右键单击解决方案,选择“添加\现有项目”

若要从 Win32 项目中使用该 WPF dll,需要添加引用:

  1. 右键单击 win32clock 项目,然后选择“引用...”

  2. 单击“添加新引用”

  3. 单击“项目”选项卡。选择 WPFClock,单击“确定”

  4. 单击“确定”退出用于添加引用的 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 内容类的构造函数创建 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 的具体位置在哪里。 可以使用 Visual Studio 对话框编辑器将 Win32 STATIC 控件置于希望时钟转到的位置(“在此处插入时钟”),并使用该控件定位 WPF 时钟。

在处理 WM_INITDIALOG 的地方,你可以使用 GetDlgItem 检索占位符 STATIC 的 HWND:

HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);

然后计算占位符 STATIC 的大小和位置,以便可以将 WPF 时钟置于该位置:

RECT 矩形;

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 时钟控件。 您主要可以在标记语言中进行此操作,只需在后台代码中使用几个事件处理程序即可。 由于本教程是关于互操作而不是控件设计,因此 WPF 时钟的完整代码作为代码块在此处提供,无需离散说明来构建它或每个部件的含义。 可以随意试验此代码,以更改控件的外观或功能。

此处为标记:

<Page x:Class="WPFClock.Clock"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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 时钟互操作示例

另请参阅