Przewodnik: hostowanie zegara WPF w Win32

Aby umieścić WPF wewnątrz aplikacji Win32, użyj polecenia HwndSource, który udostępnia HWND, który zawiera zawartość WPF. Najpierw utwórz element HwndSource, podając mu parametry podobne do createWindow. Następnie poinformujesz o HwndSource zawartości WPF, którą chcesz umieścić w niej. Na koniec uzyskasz element HWND z .HwndSource W tym przewodniku pokazano, jak utworzyć mieszany WPF wewnątrz aplikacji Win32, która ponownie zaimplementowała okno dialogowe Właściwości daty i godziny systemu operacyjnego.

Wymagania wstępne

Zobacz Współdziałanie WPF i Win32.

Jak korzystać z tego samouczka

Ten samouczek koncentruje się na ważnych krokach tworzenia aplikacji współdziałania. Samouczek jest wspierany przez przykład Win32 Clock Interoperation Sample, ale ten przykład odzwierciedla produkt końcowy. W tym samouczku opisano kroki tak, jakby rozpoczynano od istniejącego projektu Win32 własnego, być może istniejącego projektu i dodaliśmy hostowany WPF do aplikacji. Możesz porównać swój produkt końcowy z przykładem Win32 Clock Interoperation Sample.

Przewodnik po strukturze prezentacji systemu Windows w systemie Win32 (HwndSource)

Na poniższej ilustracji przedstawiono zamierzony produkt końcowy tego samouczka:

Screenshot that shows the Date and Time Properties dialog box.

To okno dialogowe można utworzyć ponownie, tworząc projekt Win32 języka C++ w programie Visual Studio i używając edytora okien dialogowych, aby utworzyć następujące elementy:

Recreated Date and Time Properties dialog box

(Nie trzeba używać programu Visual Studio do używania HwndSourceprogramu i nie trzeba używać języka C++ do pisania programów Win32, ale jest to dość typowy sposób, aby to zrobić, i nadaje się dobrze do wyjaśnienia samouczka krokowego).

Aby umieścić zegar WPF w oknie dialogowym, musisz wykonać pięć określonych kroków podrzędnych:

  1. Włącz projekt Win32 w celu wywołania kodu zarządzanego (/clr), zmieniając ustawienia projektu w programie Visual Studio.

  2. Utwórz WPFPage w oddzielnej biblioteki DLL.

  3. Umieść ten WPFPage wewnątrz .HwndSource

  4. Pobierz element HWND dla tego Page przy użyciu Handle właściwości .

  5. Użyj win32, aby zdecydować, gdzie umieścić HWND w większej aplikacji Win32

/Clr

Pierwszym krokiem jest przekształcenie tego niezarządzanego projektu Win32 w taki, który może wywoływać kod zarządzany. Użyj opcji kompilatora /clr, która będzie łączyć się z wymaganymi bibliotekami DLL, których chcesz użyć, i dostosować metodę Main do użycia z WPF.

Aby włączyć używanie kodu zarządzanego w projekcie C++: kliknij prawym przyciskiem myszy projekt win32clock i wybierz polecenie Właściwości. Na stronie Właściwości Ogólne (wartość domyślna) zmień obsługę środowiska uruchomieniowego języka wspólnego na /clr.

Następnie dodaj odwołania do bibliotek DLL niezbędnych dla bibliotekI WPF: PresentationCore.dll, PresentationFramework.dll, System.dll, WindowsBase.dll, UIAutomationProvider.dll i UIAutomationTypes.dll. (W poniższych instrukcjach założono, że system operacyjny jest zainstalowany na dysku C: ).

  1. Kliknij prawym przyciskiem myszy projekt win32clock i wybierz pozycję Odwołania..., a w tym oknie dialogowym:

  2. Kliknij prawym przyciskiem myszy projekt win32clock i wybierz pozycję Odwołania....

  3. Kliknij przycisk Dodaj nowe odwołanie, kliknij kartę Przeglądaj, wprowadź C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll, a następnie kliknij przycisk OK.

  4. Powtórz dla pliku PresentationFramework.dll: C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll.

  5. Powtórz dla pliku WindowsBase.dll: C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll.

  6. Powtórz dla UIAutomationTypes.dll: C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationTypes.dll.

  7. Powtórz dla UIAutomationProvider.dll: C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationProvider.dll.

  8. Kliknij pozycję Dodaj nowe odwołanie, wybierz pozycję System.dll, a następnie kliknij przycisk OK.

  9. Kliknij przycisk OK , aby zakończyć strony właściwości win32clock w celu dodania odwołań.

Na koniec dodaj element STAThreadAttribute do _tWinMain metody do użycia z WPF:

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

Ten atrybut informuje środowisko uruchomieniowe języka wspólnego (CLR), że podczas inicjowania modelu obiektów składników (COM) powinien używać jednego modelu apartamentów wątkowych (STA), który jest niezbędny dla WPF (i Windows Forms).

Tworzenie strony struktury prezentacji systemu Windows

Następnie utworzysz bibliotekę DLL definiującą WPFPage. Często najłatwiej jest utworzyć WPFPage jako autonomiczną aplikację i napisać i debugować część WPF w ten sposób. Po zakończeniu ten projekt można przekształcić w bibliotekę DLL, klikając projekt prawym przyciskiem myszy, klikając pozycję Właściwości, przechodząc do pozycji Aplikacja i zmieniając typ danych wyjściowych na Bibliotekę klas systemu Windows.

Projekt biblioteki DLL WPF można następnie połączyć z projektem Win32 (jednym rozwiązaniem zawierającym dwa projekty) — kliknij rozwiązanie prawym przyciskiem myszy, wybierz polecenie Dodaj\Istniejący projekt.

Aby użyć tej biblioteki dll WPF z projektu Win32, należy dodać odwołanie:

  1. Kliknij prawym przyciskiem myszy projekt win32clock i wybierz pozycję Odwołania....

  2. Kliknij pozycję Dodaj nowe odwołanie.

  3. Kliknij kartę Projekty. Wybierz pozycję WPFClock, kliknij przycisk OK.

  4. Kliknij przycisk OK , aby zakończyć strony właściwości win32clock w celu dodania odwołań.

Hwndsource

Następnie użyj HwndSource polecenia , aby WPFPage wyglądał jak HWND. Ten blok kodu można dodać do pliku 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();
    }
}
}

Jest to długi fragment kodu, który może użyć pewnych wyjaśnień. Pierwsza część to różne klauzule, dzięki czemu nie trzeba w pełni kwalifikować wszystkich wywołań:

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

Następnie zdefiniuj funkcję, która tworzy zawartość WPF, umieszcza HwndSource wokół niej i zwraca HWND:

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

Najpierw należy utworzyć obiekt HwndSource, którego parametry są podobne do polecenia 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
);

Następnie utworzysz klasę zawartości WPF, wywołując jej konstruktor:

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

Następnie połącz stronę z elementem HwndSource:

source->RootVisual = page;

W ostatnim wierszu zwróć wartość HWND dla elementu HwndSource:

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

Pozycjonowanie pionu

Teraz, gdy masz HWND, który zawiera zegar WPF, musisz umieścić ten HWND w oknie dialogowym Win32. Jeśli wiesz, gdzie umieścić HWND, wystarczy przekazać ten rozmiar i lokalizację do zdefiniowanej GetHwnd wcześniej funkcji. Ale użyto pliku zasobu do zdefiniowania okna dialogowego, więc nie masz dokładnie pewności, gdzie znajdują się żadne z rekordów HWND. Możesz użyć edytora okien dialogowych programu Visual Studio, aby umieścić kontrolkę Win32 STATIC, w której ma być ustawiony zegar ("Wstaw zegar tutaj"), i użyć go, aby ustawić zegar WPF.

Gdzie obsługujesz WM_INITDIALOG, należy użyć GetDlgItem polecenia , aby pobrać HWND dla symbolu zastępczego STATIC:

HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);

Następnie obliczysz rozmiar i położenie tego symbolu zastępczego STATIC, aby w tym miejscu umieścić zegar WPF:

Prostokąt 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);

Następnie ukrywasz symbol zastępczy STATIC:

ShowWindow(placeholder, SW_HIDE);

I utwórz zegar WPF HWND w tej lokalizacji:

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

Aby samouczek był interesujący, i aby utworzyć prawdziwy zegar WPF, musisz w tym momencie utworzyć kontrolkę zegara WPF. Można to zrobić głównie w znacznikach, z zaledwie kilkoma procedurami obsługi zdarzeń w kodzie. Ponieważ ten samouczek dotyczy współdziałania, a nie projektowania sterowania, kompletny kod zegara WPF jest dostępny tutaj jako blok kodu, bez dyskretnych instrukcji dotyczących kompilowania go lub co oznacza każda część. Możesz poeksperymentować z tym kodem, aby zmienić wygląd i działanie kontrolki.

Oto znaczniki:

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

Oto towarzyszący kod:

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

Wynik końcowy wygląda następująco:

Final result Date and Time Properties dialog box

Aby porównać wynik końcowy z kodem utworzonym na tym zrzucie ekranu, zobacz przykład Win32 Clock Interoperation Sample(Przykład interoperacji zegara Win32).

Zobacz też