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