1. 程式人生 > >在Office應用中開啟WPF窗體並且讓子窗體顯示在Office應用上

在Office應用中開啟WPF窗體並且讓子窗體顯示在Office應用上

在.NET主程式中,我們可以通過建立 ExcelApplication 物件來開啟一個Excel應用程式,如果我們想在Excle裡面再開啟WPF視窗,問題就不那麼簡單了。

我們可以簡單的例項化一個WPF窗體物件然後在Office應用程式的窗體上開啟這個新的WPF窗體,此時Office應用的窗體就是這個WPF的宿主窗體,這個WPF窗體是Office應用窗體的“子窗體”。然後子窗體跟宿主不是在一個UI執行緒上,也不在同一個程序上,子窗體很可能會在宿主窗體後面看不到。這個時候需要呼叫Win32函式,將Office應用的窗體設定為WPF子窗體的父窗體,讓WPF子窗體成為真正的“子窗體”。這個函式的形式定義如下:

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

由於Office應用程式是非託管程式,WPF窗體是託管程式,.NET提供了一個 WindowInteropHelper 包裝類,它可以將一個託管程式窗體包裝得到一個視窗控制代碼,之後,就可以呼叫上面的Win32函式 SetParent 設定視窗的父子關係了。

下面方法是一個完整的方法,可以通過反射例項化一個WPF窗體物件,然後設定此WPF窗體物件為Office應用程式的子窗體,並正常顯示在Office應用程式上。

   /// <summary>
        /// 在Excle視窗上顯示WPF窗體
        /// </summary>
        /// <param name="assemplyName">窗體物件所在程式集</param>
        /// <param name="paramClassFullName">窗體物件全名稱</param>
        public static void ExcelShowWPFWindow(string assemplyName, string paramClassFullName)
        {
            Application.Current.Dispatcher.Invoke(
new Action(() => { try { Assembly assembly = Assembly.Load(assemplyName); Type classType = assembly.GetType(paramClassFullName); object[] constuctParms = new object[] { }; dynamic view = Activator.CreateInstance(classType, constuctParms); Window winBox = view as Window; var winBoxIntreop = new WindowInteropHelper(winBox); winBoxIntreop.EnsureHandle(); //將Excel控制代碼指定為當前窗體的父窗體的控制代碼,參考 https://blog.csdn.net/pengcwl/article/details/7817111 //ExcelApp 是一個Excle應用程式物件 var excelHwnd = new IntPtr(OfficeApp.ExcelApp.Hwnd); winBoxIntreop.Owner = excelHwnd; SetParent(winBoxIntreop.Handle, excelHwnd); winBox.ShowDialog(); } catch (Exception ex) { MessageBox.Show("開啟視窗錯誤:"+ex.Message); } })); } }

 下面是開啟的效果圖:

不過,既然是的打開了一個模態視窗,我們當然是想獲得視窗的返回值。在WinForms比較簡單,但是在WPF就需要做下設定。

首先看到上圖的WPF窗體的XAML定義:

<Window x:Class="MyWPF.View.Test"
        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:MyWPF.View"
         DataContext="{Binding TestViewModel, Source={StaticResource MyViewModelLocator}}"
        mc:Ignorable="d"
        Title="Test" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="80"/>
        </Grid.RowDefinitions>

        <TextBox Text="{Binding TestVale1}"/>
        <Button Content="sure" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="TestBtn" Click="testBtn_Click"/>
    </Grid>
</Window>

窗體綁定了一個 TestViewModel1的ViewModel:

public class TestViewModel : EntityBase,IWindowReturnValue<string>
{
        public TestViewModel()
        {
        }
       
        public string TestValue1
        {
            get { return getProperty<string>("TestValue1"); }
            set {
                setProperty("TestValue1",value,1000);
                ReturnValue = value;
            }
        }
     
        public string ReturnValue { get; set; }
        public string BackTest()
        {
           return TestValue1;
        }
    }
}

TestViewModel 繼承了SOD框架的實體類基類,它可以方便的實現MVVM的依賴屬性,參考SOD的MVVM實現原理。本文重點看IWindowReturnValue<T>介面的定義:

  public interface IWindowReturnValue<T>
    {
        T ReturnValue { get; set; }
    }

介面很簡單,就是定義一個返回值屬性,這個屬性在ViewModel 裡面適當的時候給它賦值即可。

最後,我們改寫下前面的Excle開啟窗體的函式就可以了,程式碼如下:

 public static T ExcelShowWPFWindow<T>(string assemplyName, string paramClassFullName)
        {
            T result = default(T);
            Application.Current.Dispatcher.Invoke(new Action(() =>
            {
                try
                {
                    Assembly assembly = Assembly.Load(assemplyName);
                    Type classType = assembly.GetType(paramClassFullName);
                    object[] constuctParms = new object[] { };
                    dynamic view = Activator.CreateInstance(classType, constuctParms);
                    Window winBox = view as Window;
                    var winBoxIntreop = new WindowInteropHelper(winBox);
                    winBoxIntreop.EnsureHandle();
//將Excel控制代碼指定為當前窗體的父窗體的控制代碼,參考 https://blog.csdn.net/pengcwl/article/details/7817111   var excelHwnd = new IntPtr(OfficeApp.ExcelApp.Hwnd);
                    winBoxIntreop.Owner = excelHwnd;
SetParent(winBoxIntreop.Handle, excelHwnd); var dataModel = winBox.DataContext as IWindowReturnValue<T>; winBox.ShowDialog(); result = dataModel.ReturnValue; } catch (Exception ex) { MessageBox.Show("開啟視窗錯誤:" + ex.Message); } })); return result; } }

最後執行此示例,測試通過。

注意:

有時候由於某些原因,開啟的Excle或者Word視窗會跑到主程式後面去,這個時候關閉我們上面的WPF模態視窗後,就看不到Excel視窗了,這樣使用者體驗不太好。可以使用Win32的方法強行將Excel視窗再顯示在前面來,用下面這個方法:

 [DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd); 

其中 hWnd就是Excle的控制代碼。

另外還有一個問題,當用戶切換到其它程序,離開這個WPF模態子窗體,比如桌面,再切換回來,發現子窗體出不來,EXCEL彈出來一個警告對話方塊,內容大概是下面的樣子:

Microsoft Excel 正在等待其他應用程式以完成物件連結與嵌入操作。

關閉這個對話方塊,要切換到WPF模態對話方塊也難以切換回來,讓人很懊惱,軟體沒法使用了。

這個時候只需要關閉警告即可,等WPF子窗體操作完,再開啟警告。

 excelApplication.DisplayAlerts = false;