Tutorial: Crear una aplicación de Win32 que incluya contenido de WPF

Actualización: noviembre 2007

Para colocar contenido de WPF dentro de aplicaciones Win32, utilice HwndSource, que proporciona el identificador de ventana HWND que incluye el contenido de WPF. En primer lugar, se crea HwndSource, proporcionándole parámetros parecidos a los de CreateWindow. A continuación, se comunica a HwndSource el contenido de WPF que se desea colocar en su interior. Por último, se saca el HWND del objeto HwndSource. En este tutorial se muestra cómo crear una aplicación mixta de WPF dentro de una aplicación de Win32 que vuelve a implementar el cuadro de diálogo Propiedades de fecha y hora del sistema operativo.

Requisitos previos

Visite Información general sobre la interoperabilidad de WPF y Win32.

Cómo utilizar este tutorial

Este tutorial se concentra en los pasos importantes de la generación de una aplicación de interacción. El tutorial está respaldado por un ejemplo, Ejemplo Win32 Clock Interoperation, que refleja el producto final. En este tutorial se documentan los pasos como si usted comenzase con su propio proyecto de Win32 existente, quizás un proyecto que existiera previamente, y agregase contenido de WPF hospedado a su aplicación. Puede comparar su producto final con Ejemplo Win32 Clock Interoperation.

Tutorial de Windows Presentation Framework dentro de Win32 (HwndSource)

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

Cuadro de diálogo Propiedades de fecha y hora

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

Cuadro de diálogo Propiedades de fecha y hora

(No es necesario utilizar Microsoft Visual Studio para usar HwndSource, ni se necesita utilizar C++ para escribir programas de Win32, pero ésta es una manera bastante habitual de hacerlo y se presta bien a una explicación paso a paso, como la de este tutorial).

Debe realizar cinco pasos secundarios concretos para 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 los valores del proyecto en Microsoft Visual Studio.

  2. Cree un objeto Page de WPF en un archivo DLL independiente.

  3. Coloque el objeto Page de WPF dentro de un HwndSource.

  4. Obtenga un identificador de ventana HWND para ese objeto Page utilizando la propiedad Handle.

  5. Utilice Win32 para decidir dónde desea colocar el HWND dentro de la aplicación Win32 mayor.

/clr

El primer paso consiste en convertir este proyecto de Win32 no administrado en uno capaz de llamar al código administrado. Se utiliza la opción del compilador /clr, que establecerá los vínculos a los archivos DLL necesarios que desee utilizar y ajustará el método Main para su uso con WPF.

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

A continuación, agregue referencias a los archivos DLL necesarios para WPF: PresentationCore.dll, PresentationFramework.dll, System.dll, WindowsBase.dll, UIAutomationProvider.dll y UIAutomationTypes.dll. (En las instrucciones siguientes se da por hecho que el sistema operativo está instalado en la unidad C:.)

  1. Haga clic con el botón secundario en el proyecto Win32clock y seleccione Referencias.... Dentro de ese cuadro de diálogo:

  2. Haga clic con el botón secundario en el proyecto Win32clock y seleccione Referencias....

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

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

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

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

  7. Repita este paso 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 a fin de salir de las páginas de propiedades de win32clock para agregar referencias.

Por último, agregue STAThreadAttribute al método _tWinMain para su uso con WPF:

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

Este atributo indica a common language runtime (CLR) que debe utilizar un modelo de apartamentos de un único subproceso (STA) al inicializar Modelo de objetos componentes (COM); este modelo es necesario para WPF (y para formularios Windows Forms).

Crear una página de Windows Presentation Framework

Luego, se crea un archivo DLL que define un objeto Page de WPF. Con frecuencia resulta más sencillo crear el objeto Page de WPF como una aplicación independiente y escribir y depurar la parte de WPF de ese modo. Cuando haya finalizado, el proyecto podrá convertirse en un archivo DLL. Para ello, haga clic con el botón secundario en el proyecto, haga clic en Propiedades, vaya a la aplicación y cambie el tipo de salida a Biblioteca de clases de Windows.

El proyecto de DLL de WPF se puede combinar seguidamente con el proyecto Win32 (una solución que contiene dos proyectos). Haga clic con el botón secundario en la solución y seleccione Agregar Proyecto existente.

Para utilizar ese archivo DLL de WPF del proyecto de Win32, debe agregar una referencia:

  1. Haga clic con el botón secundario en el proyecto Win32clock y seleccione Referencias....

  2. Haga clic en Agregar nueva referencia.

  3. Haga clic en la ficha Proyectos. Seleccione WPFClock y haga clic en Aceptar.

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

HwndSource

A continuación, se utiliza HwndSource para que el objeto Page de WPF se parezca a un HWND. Se agrega este bloque de código a un archivo 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();
    }
}
}

Es un fragmento de código extenso, que merece algunas explicaciones. La primera parte consiste en varias cláusulas para que no tenga que certificar totalmente todas las llamadas:

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

A continuación, se define una función que crea el contenido de WPF, lo incluye en un HwndSource y devuelve el HWND:

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

En primer lugar, se crea un 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 
            );

A continuación, se crea la clase de contenido de WPF llamando a su constructor:

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

Luego se conecta la página a HwndSource:

        source->RootVisual = page;

Por último, en la línea final, se devuelve el HWND para HwndSource:

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

Colocar el HWND

Ahora que ya tiene un HWND que contiene el reloj de WPF, hay que colocarlo dentro del cuadro de diálogo de Win32. Si supiera dónde colocarlo exactamente, bastaría con pasar el tamaño y la ubicación a la función GetHwnd que se definió anteriormente. Sin embargo, se ha utilizado un archivo de recursos para definir el cuadro de diálogo, por lo que no se puede estar seguro con exactitud de la posición de ningún HWND. Puede utilizar el editor de cuadros de diálogo de Microsoft Visual Studio para colocar un control STATIC de Win32 donde desee situar el reloj ("Insert clock here"), y utilizarlo para colocar el reloj de WPF.

Cuando se administra WM_INITDIALOG, se utiliza GetDlgItem para recuperar el HWND del marcador de posición STATIC:

HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);

A continuación, se calcula el tamaño y la posición del marcador de posición STATIC, para poder colocar el reloj de WPF en ese lugar:

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

A continuación, se oculta el marcador de posición STATIC:

ShowWindow(placeholder, SW_HIDE);

Y se crea 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 generar un reloj de WPF real, deberá crear un control de reloj de WPF en este punto. Puede hacerlo principalmente en marcado, con tan sólo algunos controladores de eventos en código subyacente. Puesto que este tutorial trata sobre interoperabilidad y no sobre el diseño de controles, el código completo del reloj de WPF se proporciona en un bloque de código, sin instrucciones discretas para crearlo ni explicaciones sobre lo que significa cada parte del mismo. No dude en experimentar con este código para cambiar la apariencia y el funcionamiento o la funcionalidad del control.

Éste es el marcado:

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

Y aquí está el código subyacente que lo acompaña:

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 este aspecto:

Cuadro de diálogo Propiedades de fecha y hora

Para comparar su resultado final con el código que generó esta captura de pantalla, vea Ejemplo Win32 Clock Interoperation.

Vea también

Tareas

Ejemplo Win32 Clock Interoperation

Conceptos

Información general sobre la interoperabilidad de WPF y Win32

Referencia

HwndSource