演练:在 Win32 中承载 WPF 时钟

若要在 Win32 应用程序中放置 WPF,请使用 HwndSource,它提供包含您的 WPF 内容的 HWND。 首先,创建 HwndSource,为它指定类似于 CreateWindow 的参数。 然后,通知 HwndSource 有关您要在其中包含的 WPF 内容。 最后,从 HwndSource 获取 HWND。 此演练演示如何在 Win32 应用程序中创建一个重新实现操作系统**“日期和时间属性”**对话框的混合 WPF。

系统必备

请参见 WPF 和 Win32 互操作

如何使用本教程

本教程重点介绍生成互操作应用程序的重要步骤。 本教程与示例 Win32 Clock Interoperation Sample(Win32 时钟互操作示例)相关,但该示例只是反映了最终结果。 本教程介绍的步骤假定您正在开始操作自己的现有 Win32 项目(也许是一个已经存在的项目),并向应用程序添加一个承载的 WPF。 可以将最终结果与 Win32 Clock Interoperation Sample(Win32 时钟互操作示例)进行比较。

在 Win32 中承载 Windows Presentation Framework 的演练 (HwndSource)

下图显示了本教程预期的最终生成结果:

“日期和时间属性”对话框

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

“日期和时间属性”对话框

(您不需要通过 Microsoft Visual Studio 来使用HwndSource,也不需要使用 C++ 来编写 Win32 程序,但是通常都这么做,这样更易于分步说明教程)。

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

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

  2. 在一个单独的 DLL 中创建 WPF Page

  3. 将该 WPF Page 放入 HwndSource 中。

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

  5. 使用 Win32 来确定大型 Win32 应用程序中用于放置此 HWND 的位置。

/clr

第一步是将此非托管 Win32 项目转换为能够调用托管代码的项目。 使用 /clr 编译器选项(该选项将链接到您要使用的所需 DLL),然后调整要用于 WPF 的 Main 方法。

若要在 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)

该特性通知 common language runtime (CLR) 当它初始化Component Object Model (COM) 时,应当使用单线程的单元模型 (STA),该模型对于 WPF 和 Windows Forms是必需的。

创建 Windows Presentation Framework 页

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

然后 WPF DLL 项目可以与 Win32 项目合并(包含两个项目的一个解决方案)- 右击该解决方案,选择**“添加\现有项目”**。

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

  1. 右击 win32clock 项目,然后选择**“引用…”**。

  2. 单击**“添加新引用”**。

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

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

HwndSource

接下来,使用 HwndSource 使 WPF Page 看起来与 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 时钟控件。 一般可以在标记中完成此操作,在代码隐藏中使用几个事件处理程序即可。 由于本教程与互操作有关,而与控件设计无关,因此这里以代码块的形式提供 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 Clock Interoperation Sample(Win32 时钟互操作示例)。

请参见

参考

HwndSource

概念

WPF 和 Win32 互操作

其他资源

Win32 Clock Interoperation Sample