Compartir a través de


Tutorial: Hospedar un reloj de WPF en Win32

Para colocar WPF dentro de aplicaciones Win32, use HwndSource, que proporciona el HWND que incluye el contenido de WPF. Primero debe crear la clase HwndSource y proporciónele parámetros similares a los de CreateWindow. Luego debe indicar a HwndSource el contenido de WPF que quiere introducir. Por último, debe extraer el HWND de HwndSource. En este tutorial se muestra cómo crear un WPF mixto dentro de una aplicación Win32 que vuelva a implementar el cuadro de diálogo Propiedades de fecha y hora del sistema operativo.

Requisitos previos

Consulte Interoperabilidad de WPF y Win32.

Cómo usar este tutorial

Este tutorial se centra en los pasos importantes para generar una aplicación de interoperabilidad. El tutorial está respaldado con un ejemplo, Win32 Clock Interoperation Sample (Ejemplo de interoperación de un reloj de Win32), aunque este ejemplo refleja el producto final. En este tutorial se documentan los pasos como si empezara con un proyecto de Win32 suyo (quizá un proyecto existente) y agregase un WPF hospedado a la aplicación. Puede comparar el producto final con Win32 Clock Interoperation Sample (Ejemplo de interoperación de un reloj de Win32).

Tutorial de Windows Presentation Framework dentro de Win32 (HwndSource)

En el siguiente gráfico se muestra el producto final previsto de este tutorial:

Captura de pantalla que muestra el cuadro de diálogo Propiedades de fecha y hora.

Puede volver a crear este cuadro de diálogo creando un proyecto de Win32 de C++ en Visual Studio y usando el editor de cuadros de diálogo para crear lo siguiente:

Cuadro de diálogo Propiedades de fecha y hora recreado

(No es necesario usar Visual Studio para poder usar HwndSource, ni tampoco es necesario usar C++ para escribir programas de Win32, pero es una manera bastante habitual para hacerlo y se presta bien a las descripciones de un tutorial paso a paso).

Debe llevar a cabo cinco pasos secundarios concretos para poder colocar un reloj de WPF en el cuadro de diálogo:

  1. Habilite el proyecto de Win32 para llamar al código administrado (/clr) cambiando la configuración del proyecto en Visual Studio.

  2. Cree una clase Page de WPF en un archivo DLL independiente.

  3. Coloque esa clase Page de WPF dentro de HwndSource.

  4. Obtenga un HWND para esa clase Page mediante la propiedad Handle.

  5. Use Win32 para decidir dónde va a colocar el HWND dentro de la aplicación Win32 más grande.

/clr

El primer paso consiste en convertir este proyecto de Win32 no administrado en uno que pueda llamar al código administrado. Use la opción del compilador/clr, que se vinculará con los archivos DLL necesarios que quiera usar y ajuste el método Main para usarlo con WPF.

Para habilitar el uso del código administrado dentro del proyecto de C++: haga clic con el botón derecho en el proyecto win32clock y seleccione Propiedades. En la página de propiedades General (la página predeterminada), cambie Compatible con Common Language Runtime a /clr.

Luego, agregue referencias a los archivos DLL necesarios para WPF: PresentationCore.dll, PresentationFramework.dll, System.dll, WindowsBase.dll, UIAutomationProvider.dll y UIAutomationTypes.dll. (si sigue las instrucciones, el sistema operativo se instalará en la unidad C:).

  1. Haga clic con el botón derecho en el proyecto win32clock y seleccione Referencias.... Luego, dentro de ese cuadro de diálogo:

  2. Haga clic con el botón derecho en el proyecto win32clock y seleccione Referencias....

  3. Haga clic en Agregar nueva referencia, haga clic en la pestaña Examinar, escriba C:\Archivos de programa\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll y haga clic en Aceptar.

  4. Repita el proceso para PresentationFramework.dll: C:\Archivos de programa\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll.

  5. Repita el proceso para WindowsBase.dll: C:\Archivos de programa\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll.

  6. Repita el proceso para UIAutomationTypes.dll: C:\Archivos de programa\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationTypes.dll.

  7. Repita el proceso para UIAutomationProvider.dll: C:\Archivos de programa\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationProvider.dll.

  8. Haga clic en Agregar nueva referencia, seleccione System.dll y haga clic en Aceptar.

  9. Haga clic en Aceptar para salir de las páginas de propiedades de win32clock para agregar referencias.

Por último, agregue el STAThreadAttribute al método _tWinMain para usarlo con WPF:

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

Este atributo indica a Common Language Runtime (CLR) que, al inicializar el Modelo de objetos componentes (COM), debe usar un modelo de contenedor uniproceso (STA), necesario para WPF (y para Windows Forms).

Cree una página de Windows Presentation Framework

Después, cree un archivo DLL que defina una clase Page de WPF. A veces resulta más fácil crear la clase Page de WPF como una aplicación independiente y escribir y depurar el fragmento de WPF de esa manera. Una vez hecho, ese proyecto se puede convertir en un archivo DLL; para ello, haga clic con el botón derecho en el proyecto, haga clic en Propiedades, vaya a la aplicación y cambie el tipo Salida a Biblioteca de clases de Windows.

Luego, el proyecto dll de WPF se puede combinar con el proyecto de Win32 (una solución que contiene dos proyectos). Para ello, haga clic con el botón derecho en la solución y seleccione Agregar\Proyecto existente.

Para usar ese proyecto dll de WPF desde el proyecto de Win32, debe agregar una referencia:

  1. Haga clic con el botón derecho en el proyecto win32clock y seleccione Referencias....

  2. Haga clic en Agregar nueva referencia.

  3. Haga clic en la pestaña Proyectos. Seleccione WPFClock y haga clic en Aceptar.

  4. Haga clic en Aceptar para salir de las páginas de propiedades de win32clock para agregar referencias.

HwndSource

Luego, use HwndSource para que la clase Page de WPF se parezca a un HWND. Agregue este bloque de código a un archivo de 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();
    }
}
}

Se trata de un fragmento de código extenso que podría incluir alguna explicación. La primera parte consta de varias cláusulas para que no tenga que completar todas las llamadas:

namespace ManagedCode
{
    using namespace System;
    using namespace System::Windows;
    using namespace System::Windows::Interop;
    using namespace System::Windows::Media;

Luego, defina una función que cree el contenido de WPF, coloque HwndSource en torno a este y devuelva el HWND:

HWND GetHwnd(HWND parent, int x, int y, int width, int height) {

Primero debe crear una clase HwndSource, cuyos parámetros son similares a los de 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
);

Luego, cree la clase del contenido de WPF llamando a su constructor:

UIElement^ page = gcnew WPFClock::Clock();

Después, conecte la página a HwndSource:

source->RootVisual = page;

Y, en la línea final, devuelva el HWND de HwndSource:

return (HWND) source->Handle.ToPointer();

Colocar el HWND

Ahora que ya tiene un HWND que contiene el reloj de WPF, debe colocarlo dentro del cuadro de diálogo de Win32. Si supiera dónde debe colocar el HWND, bastaría con pasar el tamaño y la ubicación a la función GetHwnd definida anteriormente. pero ha usado un archivo de recursos para definir el cuadro de diálogo, por lo que no sabe con certeza dónde están colocados los HWND. Puede usar el editor de cuadros de diálogo de Visual Studio para colocar un control STATIC de Win32 allí donde quiera colocar el reloj ("Insertar reloj aquí") y usarlo para colocar el reloj de WPF.

Al administrar WM_INITDIALOG, use GetDlgItem para recuperar el HWND del marcador de posición STATIC:

HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);

Luego, calcule el tamaño y la posición del marcador de posición STATIC, de manera que pueda colocar el reloj de WPF en ese lugar:

RECT rectángulo;

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);

Luego, oculte el marcador de posición STATIC:

ShowWindow(placeholder, SW_HIDE);

Cree el HWND del reloj de WPF en esa ubicación:

HWND clock = ManagedCode::GetHwnd(hDlg, point.x, point.y, width, height);

Para que el tutorial resulte interesante y crear un reloj de WPF real, en este punto deberá crear un control para el reloj de WPF. Puede hacerlo principalmente en el marcado, con tan solo unos pocos controladores de eventos en el código subyacente. Dado que este tutorial trata la interoperación y no el diseño de los controles, aquí se proporciona el código completo para el reloj de WPF como un bloque de código, sin instrucciones discretas sobre cómo crearlo o sobre qué significa cada parte. No dude en experimentar con este código para cambiar el aspecto o la funcionalidad del control.

Aquí está el marcado:

<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>

Y aquí está el código subyacente adjunto:

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);
        }
    }
}

El resultado final tiene el siguiente aspecto:

Cuadro de diálogo Propiedades de fecha y hora de resultado final

Para comparar el resultado final con el código que creó esta captura de pantalla, consulte Win32 Clock Interoperation Sample (Ejemplo de interoperación de un reloj de Win32).

Vea también