1. 程式人生 > 其它 >dotnet 讀 WPF 原始碼筆記 使用 Win32 方法修改視窗的座標和大小對視窗依賴屬性的影響...

dotnet 讀 WPF 原始碼筆記 使用 Win32 方法修改視窗的座標和大小對視窗依賴屬性的影響...

技術標籤:WPF原始碼c#WPFdotnetC#WPF

咱可以使用 Win32 的 SetWindowPos 修改視窗的座標和大小,此時 WPF 的視窗的 Left 和 Top 和 Width 和 Height 依賴屬性也會受到影響,本文將會告訴大家在啥時候會同步更改 WPF 依賴屬性的值,而什麼時候不會

本文將會用到很多 Win32 方法,在 dotnet 基金會開源了對 win32 等的呼叫的封裝庫,請看 https://github.com/dotnet/pinvoke

本文程式碼放在 github 歡迎小夥伴訪問

在開始之前,咱先寫一個 XAML 介面,用來繫結 Window 的依賴屬性。以及加上幾個按鈕,用來使用 Win32 方法修改視窗座標或大小

<Window x:Class="FurnaheaneHejichaijair.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FurnaheaneHejichaijair"
        mc:Ignorable="d"
        x:Name="Root"
        Title="MainWindow" Height="450" Width="800">
  <Grid>
    <StackPanel>
      <TextBlock FontSize="50" Text="{Binding ElementName=Root,Path=Left}" />
      <TextBlock FontSize="50" Text="{Binding ElementName=Root,Path=Top}" />
      <TextBlock FontSize="50" Text="{Binding ElementName=Root,Path=Width}" />
      <TextBlock FontSize="50" Text="{Binding ElementName=Root,Path=Height}" />

      <Button x:Name="PositionButton" Margin="10,10,10,10" 
              HorizontalAlignment="Left" Content="修改座標" 
              Click="PositionButton_OnClick"></Button>
      <Button Margin="10,10,10,10" 
              HorizontalAlignment="Left" Content="修改大小" 
              Click="SizeButton_OnClick"></Button>
      <Button x:Name="SetWindowLongPtrButton" Margin="10,10,10,10" 
              HorizontalAlignment="Left" Content="SetWindowLongPtr"
              Click="SetWindowLongPtrButton_OnClick"></Button>
    </StackPanel>
  </Grid>
</Window>

可以看到在完成了上面介面之後,在拖動視窗,以及修改視窗大小的時候,都可以看到值是對應變化的。接下來咱來試試 Win32 的方法來修改

在 PositionButton_OnClick 方法裡面新增對視窗修改座標的方法

        public const string LibraryName = "user32";

        private void PositionButton_OnClick(object sender, RoutedEventArgs e)
        {
            var windowInteropHelper = new WindowInteropHelper(this);
            var SWP_NOSIZE = 0x0001;
            SetWindowPos(windowInteropHelper.Handle, IntPtr.Zero, (int)(Left + 10), (int)(Top + 10), 0, 0, SWP_NOSIZE);
        }

        /// <summary>
        /// 改變一個子視窗、彈出式視窗和頂層視窗的尺寸、位置和 Z 序。
        /// </summary>
        /// <param name="hWnd">視窗控制代碼。</param>
        /// <param name="hWndInsertAfter">
        /// 在z序中的位於被置位的視窗前的視窗控制代碼。該引數必須為一個視窗控制代碼,或下列值之一:
        /// <para>HWND_BOTTOM:將視窗置於 Z 序的底部。如果引數hWnd標識了一個頂層視窗,則視窗失去頂級位置,並且被置在其他視窗的底部。</para>
        /// <para>HWND_NOTOPMOST:將視窗置於所有非頂層視窗之上(即在所有頂層視窗之後)。如果視窗已經是非頂層視窗則該標誌不起作用。</para>
        /// <para>HWND_TOP:將視窗置於Z序的頂部。</para>
        /// <para>HWND_TOPMOST:將視窗置於所有非頂層視窗之上。即使視窗未被啟用視窗也將保持頂級位置。</para>
        /// 如無須更改,請使用 IntPtr.Zero 的值
        /// </param>
        /// <param name="x">以客戶座標指定視窗新位置的左邊界。</param>
        /// <param name="y">以客戶座標指定視窗新位置的頂邊界。</param>
        /// <param name="cx">以畫素指定視窗的新的寬度。如無須更改,請在 <paramref name="wFlagslong"/> 設定 <see cref="WindowPositionFlags.SWP_NOSIZE"/> 的值 </param>
        /// <param name="cy">以畫素指定視窗的新的高度。如無須更改,請在 <paramref name="wFlagslong"/> 設定 <see cref="WindowPositionFlags.SWP_NOSIZE"/> 的值</param>
        /// <param name="wFlagslong">
        /// 可傳入 <see cref="WindowPositionFlags"/> 列舉中的值
        /// 視窗尺寸和定位的標誌。該引數可以是下列值的組合:
        /// <para>SWP_ASYNCWINDOWPOS:如果呼叫程序不擁有視窗,系統會向擁有視窗的執行緒發出需求。這就防止呼叫執行緒在其他執行緒處理需求的時候發生死鎖。</para>
        /// <para>SWP_DEFERERASE:防止產生 WM_SYNCPAINT 訊息。</para>
        /// <para>SWP_DRAWFRAME:在視窗周圍畫一個邊框(定義在視窗類描述中)。</para>
        /// <para>SWP_FRAMECHANGED:給視窗傳送 WM_NCCALCSIZE 訊息,即使視窗尺寸沒有改變也會發送該訊息。如果未指定這個標誌,只有在改變了視窗尺寸時才傳送 WM_NCCALCSIZE。</para>
        /// <para>SWP_HIDEWINDOW:隱藏視窗。</para>
        /// <para>SWP_NOACTIVATE:不啟用視窗。如果未設定標誌,則視窗被啟用,並被設定到其他最高階視窗或非最高階組的頂部(根據引數hWndlnsertAfter設定)。</para>
        /// <para>SWP_NOCOPYBITS:清除客戶區的所有內容。如果未設定該標誌,客戶區的有效內容被儲存並且在視窗尺寸更新和重定位後拷貝回客戶區。</para>
        /// <para>SWP_NOMOVE:維持當前位置(忽略X和Y引數)。</para>
        /// <para>SWP_NOOWNERZORDER:不改變 Z 序中的所有者視窗的位置。</para>
        /// <para>SWP_NOREDRAW:不重畫改變的內容。如果設定了這個標誌,則不發生任何重畫動作。適用於客戶區和非客戶區(包括標題欄和滾動條)和任何由於窗回移動而露出的父視窗的所有部分。如果設定了這個標誌,應用程式必須明確地使視窗無效並區重畫視窗的任何部分和父視窗需要重畫的部分。</para>
        /// <para>SWP_NOREPOSITION:與 SWP_NOOWNERZORDER 標誌相同。</para>
        /// <para>SWP_NOSENDCHANGING:防止視窗接收 WM_WINDOWPOSCHANGING 訊息。</para>
        /// <para>SWP_NOSIZE:維持當前尺寸(忽略 cx 和 cy 引數)。</para>
        /// <para>SWP_NOZORDER:維持當前 Z 序(忽略 hWndlnsertAfter 引數)。</para>
        /// <para>SWP_SHOWWINDOW:顯示視窗。</para>
        /// </param>
        /// <returns>如果函式成功,返回值為非零;如果函式失敗,返回值為零。若想獲得更多錯誤訊息,請呼叫 GetLastError 函式。</returns>
        [DllImport(LibraryName, ExactSpelling = true, SetLastError = true)]
        public static extern Int32 SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, Int32 x, Int32 y, Int32 cx,
            Int32 cy, Int32 wFlagslong);

可以看到點選修改座標按鈕,就可以修改視窗的座標,此時點選的時候,依賴屬性也跟隨變化

再來實現修改視窗大小的方法,點選方法將呼叫 SetWindowPos 方法修改視窗的寬度和高度

        private void SizeButton_OnClick(object sender, RoutedEventArgs e)
        {
            var windowInteropHelper = new WindowInteropHelper(this);
            var SWP_NOMOVE = 0x0002;
            SetWindowPos(windowInteropHelper.Handle, IntPtr.Zero, 0, 0, (int)(Width + 10), (int)(Height + 10), SWP_NOMOVE);
        }

此時點選修改視窗大小的按鈕,通過 Win32 方法修改視窗大小,也可以看到依賴屬性也進行變化。但如果此時咱點選一下最大化,那麼點選修改視窗座標按鈕,是可以修改視窗座標的,同時視窗的狀態依然是最大化。但是此時的依賴屬性沒有跟隨變化

原因還需要從完全開源的 WPF 倉庫裡面瞭解,官方的開源倉庫放在 https://github.com/dotnet/wpf 歡迎大家下載所有原始碼

src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Window.cs 檔案裡面有以下的定義

        /// <summary>
        ///     This is the hook to HwndSource that is called when window messages related to
        ///     this window occur. Currently, we listen to the following messages
        ///
        ///         WM_CLOSE        : We listen to this message in order to fire the Closing event.
        ///                           If the user cancels window closing, we set handled to true so
        ///                           that the DefWindowProc does not handle this message. Otherwise,
        ///                           we set handled to false.
        ///         WM_DESTROY      : We listen to this message in order to fire the Closed event.
        ///                           Handled is always set to false.
        ///         WM_ACTIVATE     : Used for Activated and deactivated events
        ///         WM_SIZE         : Used for SizeChanged, StateChanged events. Also, helps us keep our
        ///                           size updated
        ///         WM_MOVE:        : Used for location changed event and to keep our cached top/left
        ///                           updated
        ///         WM_GETMINMAXINFO: Used to enforce Max/MinHeight and Max/MinWidth
        /// </summary>
        /// <param name="hwnd"></param>
        /// <param name="msg"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <param name="handled"></param>
        /// <returns></returns>
        private IntPtr WindowFilterMessage( IntPtr hwnd,
            int msg,
            IntPtr wParam,
            IntPtr lParam,
            ref bool handled)
        {

        }

在這個方法裡面,將會從 Win 訊息拿到對應的值分發給對應的方法處理,如下面程式碼

switch (message)
{
    case WindowMessage.WM_CLOSE:
        handled = WmClose();
        break;
    case WindowMessage.WM_DESTROY:
        handled = WmDestroy();
        break;
    case WindowMessage.WM_ACTIVATE:
        handled = WmActivate(wParam);
        break;
    case WindowMessage.WM_MOVE: // 視窗移動
        handled = WmMoveChanged();
        break;
    case WindowMessage.WM_NCHITTEST:
        handled = WmNcHitTest(lParam, ref retInt);
        break;
    case WindowMessage.WM_SHOWWINDOW:
        handled = WmShowWindow(wParam, lParam);
        break;
    case WindowMessage.WM_COMMAND:
        handled = WmCommand(wParam, lParam);
        break;
    default:
        handled = false;
        break;
}

WindowMessage.WM_MOVE 訊息裡面,將會呼叫到 WmMoveChanged 方法,這個方法的邏輯大概如下

        private bool WmMoveChanged()
        {
        	// 在 WindowBounds 屬性裡面,將會獲取當前 Win32 視窗的座標和大小
            // the input lparam gives the client location,
            // so just call GetWindowRect for Left and Top.
            NativeMethods.RECT rc = WindowBounds;

            // 此時需要將螢幕的座標轉換為 WPF 的座標
            Point ptLogicalUnits = DeviceToLogicalUnits(new Point(rc.left, rc.top));

            // 如果值更新了,那麼將會更新 _actualLeft 和 _actualTop 屬性
            if (!DoubleUtil.AreClose(_actualLeft, ptLogicalUnits.X) ||
                !DoubleUtil.AreClose(_actualTop, ptLogicalUnits.Y))
            {
                _actualLeft = ptLogicalUnits.X;
                _actualTop = ptLogicalUnits.Y;

                // In Window, WmMoveChangedHelper write the local value of Top/Left
                // (if necessary) or updates the property system values for
                // Top/Left by calling CoerceValue.  Furthermore, it fires the
                // LocationChanged event.  RBW overrides WmMoveChangedHelper to do
                // nothing as writing Top/Left is not supported for RBW and
                // LocationChanged is never fired for it either.
                WmMoveChangedHelper();
            }

            return false;
        }

        private NativeMethods.RECT WindowBounds
        {
            get
            {
                Debug.Assert( _swh != null );
                return _swh.WindowBounds;
            }
        }

_swh.WindowBounds 通用也是一個只有 get 的屬性,定義如下

        internal class SourceWindowHelper
        {
                internal NativeMethods.RECT WindowBounds
                {
                    get
                    {
                        NativeMethods.RECT rc = new NativeMethods.RECT(0,0,0,0);
                        SafeNativeMethods.GetWindowRect(new HandleRef(this, CriticalHandle), ref rc);

                        return rc;
                    }
                }
        }

也就是說本質是通過 User32.dll 的 GetWindowRect 方法獲取 Win32 視窗的座標和大小

而更改依賴屬性的邏輯是放在 WmMoveChangedHelper 方法的,程式碼如下

        internal void WmMoveChangedHelper()
        {
        	// 如果視窗是最大化,不更新依賴屬性,但是視窗最大化可以通過 Win32 方法修改視窗座標和大小,此時的依賴屬性就沒有和實際視窗的座標相同
            if (WindowState == WindowState.Normal)
            {
                try
                {
                    _updateHwndLocation = false;
                    // 更新依賴屬性
                    SetValue(LeftProperty, _actualLeft);
                    SetValue(TopProperty, _actualTop);
                }
                finally
                {
                    _updateHwndLocation = true;
                }

                // Event handler exception continuality: if exception occurs in LocationChanged event handler, our state will not be
                // corrupted because the states related to LocationChanged, LeftProperty, TopProperty, Left and Top are set before the event is fired.
                // Please check event handler exception continuality if the logic changes.
                OnLocationChanged(EventArgs.Empty);
            }
        }

可以看到在 WmMoveChangedHelper 方法裡面會判斷 WindowState == WindowState.Normal 才會更新 Left 和 Top 依賴屬性。這就是為什麼最大化的時候修改座標不會更新依賴屬性

另外在 WmMoveChanged 方法的實現裡面,可以看到一個坑,在判斷是否需要更新的時候,是採用 _actualLeft_actualTop 判斷的

 // 如果值更新了,那麼將會更新 _actualLeft 和 _actualTop 屬性
 if (!DoubleUtil.AreClose(_actualLeft, ptLogicalUnits.X) ||
     !DoubleUtil.AreClose(_actualTop, ptLogicalUnits.Y))
 {
 	// 忽略程式碼
 }

如果此時我在使用 Win32 更改的過程中,也修改了 Left 和 Top 依賴屬性呢?可以看到此時的 _actualLeft_actualTop 和 Win32 相同,此時就不會再次呼叫更新了,此時的 Left 和 Top 依賴屬性就沒有和 Win32 同步了

上面是說到的是修改視窗的座標,那如果修改的是視窗的大小呢?在 WindowFilterMessage 方法裡面,除了呼叫 WmMoveChanged 方法外,還有以下程式碼

            switch (message)
            {
                case WindowMessage.WM_GETMINMAXINFO:
                    handled = WmGetMinMaxInfo(lParam);
                    break;
                case WindowMessage.WM_SIZE:
                    handled = WmSizeChanged(wParam);
                    break;
            }

可以看到在訊息是 WindowMessage.WM_SIZE 將會呼叫 WmSizeChanged 方法,這個方法的邏輯如下

        private bool WmSizeChanged(IntPtr wParam)
        {
            // 呼叫 WindowBounds 屬性,獲取當前的座標
            NativeMethods.RECT rc = WindowBounds;
            // 計算視窗的大小,儘管使用的是 Point 但實際含義是 Size 哦,原因是為了重複呼叫 DeviceToLogicalUnits 方法而已
            Point windowSize = new Point(rc.right - rc.left, rc.bottom - rc.top);
            // 轉換為 WPF 座標,這裡的 Point 其實是 Size 哈,只是 WPF 的開發者 hack 一下,使用 DeviceToLogicalUnits 方法返回的 Point 而已
            Point ptLogicalUnits = DeviceToLogicalUnits(windowSize);

            try
            {
            	// 修改依賴屬性
                _updateHwndSize = false;
                SetValue(FrameworkElement.WidthProperty, ptLogicalUnits.X);
                SetValue(FrameworkElement.HeightProperty, ptLogicalUnits.Y);
            }
            finally
            {
                _updateHwndSize = true;
            }

            // 忽略程式碼

            return false;
        }

因此 WPF 的依賴屬性是根據 Windows 訊息,更新依賴屬性的,而在 Left 和 Top 屬性的更新裡面,會先判斷 _actualLeft_actualTop 是否和 Win32 的相同,如果相同就不更新,因此行為上和寬度和高度的屬性有點差別。另外最大化也會影響 Left 和 Top 屬性,因為在更新這兩個屬性之前會先判斷視窗,如果是最大化的,將不會更新這兩個依賴屬性。但是寬度和高度屬性就沒有這個判斷

當前的 WPF 在 https://github.com/dotnet/wpf 完全開源,使用友好的 MIT 協議,意味著允許任何人任何組織和企業任意處置,包括使用,複製,修改,合併,發表,分發,再授權,或者銷售。在倉庫裡面包含了完全的構建邏輯,只需要本地的網路足夠好(因為需要下載一堆構建工具),即可進行本地構建

我搭建了自己的部落格 https://blog.lindexi.com/ 歡迎大家訪問,裡面有很多新的部落格。只有在我看到部落格寫成熟之後才會放在csdn或部落格園,但是一旦釋出了就不再更新

如果在部落格看到有任何不懂的,歡迎交流,我搭建了 dotnet 職業技術學院 歡迎大家加入

如有不方便在部落格評論的問題,可以加我 QQ 2844808902 交流

知識共享許可協議
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名林德熙(包含連結:http://blog.csdn.net/lindexi_gd ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯絡。