在開發XX新聞的過程中,UI部分使用了Master/Detail(大綱/細節)佈局樣式。Win10系統中的郵件App就是這種樣式,左側一個列表,右側是詳情頁面。關於這種 樣式的說明可參看MSDN文檔:https://msdn.microsoft.com/zh-cn/library/windows/a
在開發XX新聞的過程中,UI部分使用了Master/Detail(大綱/細節)佈局樣式。Win10系統中的郵件App就是這種樣式,左側一個列表,右側是詳情頁面。關於這種 樣式的說明可參看MSDN文檔:https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/dn997765.aspx
樣式如下:
在微軟官方的Sample里,有這種樣式的代碼示例,下載地址:https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlMasterDetail
這個例子可以拿來直接用。處理這種佈局,主要是需要處理在PC/Mobile不同屏幕寬度下的具體顯示內容,可以使用VisualState來實現不同狀態的切換。
為了將WP8.1版本的項目快速升級到UWP版本,我沒有使用官方示例的方式,而是在MainPage里放了兩個Frame,在左側的Frame里放一個列表Page,右側Frame里放一個詳情Page,這樣之前的頁面的UI和ViewModel都可以原封不動的拿過來,只需要單獨處理VisualState的切換就可以了。
下麵以一個簡單的例子來說一下是如何實現的。最近關於Win10 UWP的內容寫了不少,都是在開發的過程中,把能單獨拿出來的部分再重新做一遍demo,所以如果大家有興趣的話可以照著動手敲一遍,自己實現出來才會理解的更深入。
一、新建項目及Model
首先新建一個MVVM-Sidekick項目,命名為MasterDetailDemo。
添加Models目錄,新建一個NewsItem:
public class NewsItem { public int Id { get; set; } public DateTime DateCreated { get; set; } public string Title { get; set; } public string Text { get; set; } }
新建一個ItemsDataSource類,用於模擬數據,可以返回一些數據。具體代碼看Demo里的。
private static List<NewsItem> _items = new List<NewsItem>() { new NewsItem() {}…… } public static IList<NewsItem> GetAllItems() { return _items; } public static NewsItem GetItemById(int id) { return _items[id]; }
二、頁面佈局
在MainPage中放置一個Grid控制項,分為兩列,左側和右側分別放兩個Frame控制項:
<Grid x:Name="gridMain" > <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="0" /> </Grid.ColumnDefinitions> <Frame x:Name="masterFrame" Grid.Column="0" mvvm:StageManager.Beacon="masterFrame" x:FieldModifier="public"/> <Frame x:Name="detailFrame" HorizontalAlignment="Stretch" Grid.Column="1" mvvm:StageManager.Beacon="detailFrame" x:FieldModifier="public"> </Frame> </Grid>
然後添加兩個頁面,MasterPage和DetailPage。
MasterPage中放置一個ListView控制項,調用剛纔的ItemsDataSource類,把數據綁定到ListView上,這個就不用詳述了吧。還要給ListView設置項模板。這部分代碼就不貼了。
現在讓MainPage頁面載入時,左側的Frame自動顯示MasterPage。
打開MainPage_Model.cs文件,取消對OnBindedViewLoad方法的註釋,修改為以下代碼:
protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view) { await base.OnBindedViewLoad(view); await StageManager["masterFrame"].Show(new MasterPage_Model()); }
好了,現在當MainPage頁面載入完成後,名為masterFrame的Frame會顯示MasterPage的內容,像下麵這樣:
然後要實現點擊項的時候,要在右側的Frame里顯示DetailPage。
打開MasterPage.xaml,在頭部引入以下幾個命名空間:
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
xmlns:Behaviors="using:MVVMSidekick.Behaviors"
然後修改項模板,使用SendToEventRouterAction,這個東東在以前的Blog里說過,在項模板的Grid里添加以下代碼:
<Interactivity:Interaction.Behaviors> <Core:EventTriggerBehavior EventName="Tapped"> <Behaviors:SendToEventRouterAction EventRoutingName="NewsItemTapped" EventData="{Binding}" IsEventFiringToAllBaseClassesChannels="True" /> </Core:EventTriggerBehavior> </Interactivity:Interaction.Behaviors>
在MainPage載入的時候,註冊NewsItemTapped事件,來處理點擊事件。打開MainPage_Model.cs文件,在最後添加一個RegisterCommand方法:
private void RegisterCommand() { MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>() .Where(x => x.EventName == "NewsItemTapped") .Subscribe( e => { NewsItem item = e.EventData as NewsItem; await StageManager["detailFrame"].Show(new DetailPage_Model(item)); } ).DisposeWith(this); }
別忘了在Loaded事件里調用這個方法:
protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view) { this.RegisterCommand(); await base.OnBindedViewLoad(view); await StageManager["masterFrame"].Show(new MasterPage_Model()); }
因為DetailPage_Model還沒有可接收參數的構造函數,所以需要在DetailPage_Model裡加兩個構造函數,一個是無參的,一個是可接收參數的,同時還需要加一個可綁定的屬性,用來顯示內容:
public DetailPage_Model() { } public DetailPage_Model(NewsItem item) { CurrentNewsItem = item; } public NewsItem CurrentNewsItem { get { return _CurrentNewsItemLocator(this).Value; } set { _CurrentNewsItemLocator(this).SetValueAndTryNotify(value); } } #region Property NewsItem CurrentNewsItem Setup protected Property<NewsItem> _CurrentNewsItem = new Property<NewsItem> { LocatorFunc = _CurrentNewsItemLocator }; static Func<BindableBase, ValueContainer<NewsItem>> _CurrentNewsItemLocator = RegisterContainerLocator<NewsItem>("CurrentNewsItem", model => model.Initialize("CurrentNewsItem", ref model._CurrentNewsItem, ref _CurrentNewsItemLocator, _CurrentNewsItemDefaultValueFactory)); static Func<NewsItem> _CurrentNewsItemDefaultValueFactory = () => { return default(NewsItem); }; #endregion
這樣在DetailPage里就可以接收到點擊的是哪個NewsItem了,再綁定到界面上,我就隨便放了個TextBlock:
<StackPanel x:Name="RootPanel" Grid.Row="1"> <TextBlock Margin="8,0" Style="{ThemeResource TitleTextBlockStyle}" HorizontalAlignment="Left" Text="{Binding CurrentNewsItem.Title}" /> <TextBlock Margin="12,8" HorizontalAlignment="Left" MaxWidth="560" Style="{ThemeResource BodyTextBlockStyle}" Text="{Binding CurrentNewsItem.Text}" EntranceNavigationTransitionInfo.IsTargetElement="True" /> </StackPanel>
運行一下看看,怎麼點了沒反應呢,原來在MainPage的Grid里,第一列就把寬度占滿了,第二列無法顯示了,來給兩個列設置個寬度吧,第一列設置為2*,第二列設置為3*:
現在可以顯示了:
三、自定義StateTrigger
但是,這隻是第一步,接下來需要處理在不同屏幕寬度下的適配問題,我們可以打開UWP版的郵件,拖動視窗縮放大小,觀察頁面內容變化,可以得出以下特性:
在PC上:
1、當寬度大於一定寬度時,Master和Detail是可以同時顯示的,在剛打開程式沒有點擊郵件的時候,右側的Detail實際上顯示了一個空頁面(有背景圖片);
當逐步縮小寬度時,又分為兩種情況:
2、如果Detail頁為空頁面時,縮小到一定寬度後,視窗只顯示Master頁面;
3、如果Detail頁不為空,即顯示郵件正文的時候,縮小到一定寬度後,視窗只顯示Detail頁面;
在Mobile上:
程式打開時,顯示Master頁面,相當於2;
點擊郵件後,顯示Detail頁面,相當於3;
這樣我們可以得出,不管是PC還是Mobile,需要處理三種狀態的切換,我命名為:
NarrowAndBlankDetail
NarrowAndNoBlankDetail
Wide
通過處理這三種狀態的切換,就可以實現類似郵件UWP版的效果了。有些同學可能會問,為什麼不直接使用自帶的AdaptiveTrigger呢,主要是這個AdaptiveTrigger只能根據寬度來設置,而目前的需求還需要根據Detail頁面是否為空來處理,所以需要自定義一個Trigger了。
郵件UWP預設載入的時候有一個空頁面,所以還需要添加一個BlankPage,這個頁面相當於一個空頁面,裡面可以隨便放點什麼東西,比如背景圖片啊,logo啊,或者廣告什麼的,Trigger會根據Detail頁面是否顯示這個BlankPage來進行處理。
先讓MainPage載入時,預設左側載入MasterPage,右側載入BlankPage:
protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view) { this.RegisterCommand(); await base.OnBindedViewLoad(view); StageManager["detailFrame"].Show(new BlankPage_Model()); await StageManager["masterFrame"].Show(new MasterPage_Model()); }
好,運行效果是這樣:
現在來處理狀態切換。關於StateTrigger,國外已經有人寫了一個項目,實現了多種Trigger,見:
https://github.com/dotMorten/WindowsStateTriggers
使用介紹見:http://www.sharpgis.net/post/2015/03/24/Using-Custom-Visual-State-Triggers
這個項目實現了n個實用的Trigger,但好可惜沒有能滿足我的要求的,還是自己動手吧。我參考了他的代碼,繼承了他的介面ITriggerValue,繼承此介面的話可以用在CompositeStateTrigger里,為了方便以後使用按照這個介面來吧。
說一下主要的代碼實現思路。
首先要定義一個枚舉:
public enum MasterDetailState { /// <summary> /// narrow and a blank detail page /// </summary> NarrowAndBlankDetail, /// <summary> /// narrow and detail page is not blank /// </summary> NarrowAndNoBlankDetail, /// <summary> /// wide /// </summary> Wide }
頁面寬度的變化,通過訂閱ApplicationView.GetForCurrentView().VisibleBoundsChanged事件來處理,如果寬度大於720時如何,小於720時如何。當然這個720也可以傳遞屬性進來,我懶得弄就寫死在裡面了。
同時,還需要一個DetailContent屬性,這個屬性需要綁定到第二個Frame的Content上,這樣Trigger可以知道當前是不是BlankPage,通過以下代碼來判斷:
MVVMPage detailPage = (MVVMSidekick.Views.MVVMPage)DetailContent; if (detailPage != null) { if (detailPage.BaseUri.ToString() == "ms-appx:///BlankPage.xaml") { System.Diagnostics.Debug.WriteLine("觸發NarrowAndBlankDetail模式"); //CommonContext.Instance.CurrentBackRequestedHandlerType = BackRequestedHandlerType.MasterPage; return MasterDetailState.NarrowAndBlankDetail; } else { System.Diagnostics.Debug.WriteLine("觸發NarrowAndNoBlankDetail模式"); //CommonContext.Instance.CurrentBackRequestedHandlerType = BackRequestedHandlerType.DetailPage; return MasterDetailState.NarrowAndNoBlankDetail; } }
全部代碼如下:
public class MasterDetailStateTrigger : StateTriggerBase, ITriggerValue { public MasterDetailStateTrigger() { if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled) { var weakEvent = new WeakEventListener<MasterDetailStateTrigger, ApplicationView, object>(this) { OnEventAction = (instance, source, eventArgs) => MasterDetailStatetateTrigger_MasterDetailStateChanged(source, eventArgs), OnDetachAction = (instance, weakEventListener) => ApplicationView.GetForCurrentView().VisibleBoundsChanged -= weakEventListener.OnEvent }; ApplicationView.GetForCurrentView().VisibleBoundsChanged += weakEvent.OnEvent; } } private void MasterDetailStatetateTrigger_MasterDetailStateChanged(ApplicationView sender, object args) { UpdateTrigger(); } private void UpdateTrigger() { IsActive = GetMasterDetailState() == MasterDetailState; } public MasterDetailState MasterDetailState { get { return (MasterDetailState)GetValue(MasterDetailStateProperty); } set { SetValue(MasterDetailStateProperty, value); } } // Using a DependencyProperty as the backing store for MasterDetailState. This enables animation, styling, binding, etc... public static readonly DependencyProperty MasterDetailStateProperty = DependencyProperty.Register("MasterDetailState", typeof(MasterDetailState), typeof(MasterDetailStateTrigger), new PropertyMetadata(MasterDetailState.Wide, OnMasterDetailStatePropertyChanged)); private static void OnMasterDetailStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var obj = (MasterDetailStateTrigger)d; if (!Windows.ApplicationModel.DesignMode.DesignModeEnabled) { obj.UpdateTrigger(); } } public object DetailContent { get { return (object)GetValue(DetailContentProperty); } set { SetValue(DetailContentProperty, value); } } // Using a DependencyProperty as the backing store for DetailContent. This enables animation, styling, binding, etc... public static readonly DependencyProperty DetailContentProperty = DependencyProperty.Register("DetailContent", typeof(object), typeof(MasterDetailStateTrigger), new PropertyMetadata(null, new PropertyChangedCallback(OnValuePropertyChanged))); private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var obj = (MasterDetailStateTrigger)d; obj.UpdateTrigger(); } internal MasterDetailState GetMasterDetailState() { System.Diagnostics.Debug.WriteLine("DetailContent為空:" + (DetailContent == null).ToString()); //第一種 窄屏模式 DetailFrame為空 if (Window.Current.Bounds.Width < 720) { System.Diagnostics.Debug.WriteLine("VisibleBounds.Width:" + ApplicationView.GetForCurrentView().VisibleBounds.Width.ToString()); System.Diagnostics.Debug.WriteLine("Window.Current.Bounds:" + Window.Current.Bounds.Width.ToString()); MVVMPage detailPage = (MVVMSidekick.Views.MVVMPage)DetailContent; if (detailPage != null) { if (detailPage.BaseUri.ToString() == "ms-appx:///BlankPage.xaml") { System.Diagnostics.Debug.WriteLine("觸發NarrowAndBlankDetail模式"); return MasterDetailState.NarrowAndBlankDetail; } else { System.Diagnostics.Debug.WriteLine("觸發NarrowAndNoBlankDetail模式"); return MasterDetailState.NarrowAndNoBlankDetail; } } else { return MasterDetailState.NarrowAndBlankDetail; } } else { System.Diagnostics.Debug.WriteLine("觸發Wide模式"); return MasterDetailState.Wide; } } #region ITriggerValue private bool m_IsActive; /// <summary> /// Gets a value indicating whether this trigger is active. /// </summary> /// <value><c>true</c> if this trigger is active; otherwise, <c>false</c>.</value> public bool IsActive { get { return m_IsActive; } private set { if (m_IsActive != value) { m_IsActive = value; base.SetActive(value); if (IsActiveChanged != null) IsActiveChanged(this, EventArgs.Empty); } } } /// <summary> /// Occurs when the <see cref="IsActive" /> property has changed. /// </summary> public event EventHandler IsActiveChanged; #endregion ITriggerValue } public enum MasterDetailState { /// <summary> /// narrow and a blank detail page /// </summary> NarrowAndBlankDetail, /// <summary> /// narrow and detail page is not blank /// </summary> NarrowAndNoBlankDetail, /// <summary> /// wide /// </summary> Wide }View Code
我在代碼里輸出了一些信息,調試的時候可以觀察各種狀態是在什麼時候切換的。
然後在MainPage.xaml里 應用這個StateTrigger,首先,要在MainPage的ViewModel里添加一個object,用於綁定DetailFrame的內容:
/// <summary> /// detailFrame的內容 /// </summary> public object DetailContent { get { return _DetailContentLocator(this).Value; } set { _DetailContentLocator(this).SetValueAndTryNotify(value); } } #region Property object DetailContent Setup protected Property<object> _DetailContent = new Property<object> { LocatorFunc = _DetailContentLocator }; static Func<BindableBase, ValueContainer<object>> _DetailContentLocator = RegisterContainerLocator<object>("DetailContent", model => model.Initialize("DetailContent", ref model._DetailContent, ref _DetailContentLocator, _DetailContentDefaultValueFactory)); static Func<object> _DetailContentDefaultValueFactory = () => default(object); #endregion
MainPage.xaml里的第二個Frame的Content綁定到這個DetailContent上:
<Frame x:Name="detailFrame" Content="{Binding DetailContent,Mode=TwoWay}" HorizontalAlignment="Stretch" Grid.Column="1" mvvm:StageManager.Beacon="detailFrame" x:FieldModifier="public">
註意Mode要設置為TwoWay,這樣才可以讓Trigger知道DetaiFrame的內容。在MainPage.xaml的根Grid里添加以下Trigger:
<VisualStateManager.VisualStateGroups> <VisualStateGroup> <VisualState x:Name="NarrowAndBlankDetail"> <VisualState.StateTriggers> <triggers:MasterDetailStateTrigger MasterDetailState="NarrowAndBlankDetail" DetailContent="{Binding DetailContent}" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="gridMain.ColumnDefinitions[0].Width" Value="*" /> <Setter Target="gridMain.ColumnDefinitions[1].Width" Value="0" /> </VisualState.Setters> </VisualState> <VisualState x:Name="NarrowAndNoBlankDetail"> <VisualState.StateTriggers> <triggers:MasterDetailStateTrigger MasterDetailState="NarrowAndNoBlankDetail" DetailContent="{Binding DetailContent}" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="gridMain.ColumnDefinitions[0].Width" Value="0" /> <Setter Target="gridMain.ColumnDefinitions[1].Width" Value="*" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Wide"> <VisualState.StateTriggers> <triggers:MasterDetailStateTrigger MasterDetailState="Wide" DetailContent="{Binding DetailContent}" /> </VisualState.StateTriggers> <VisualState.Setters> <Setter Target="gridMain.ColumnDefinitions[0].Width" Value="2*" /> <Setter Target="gridMain.ColumnDefinitions[1].Width" Value="3*" /> </VisualState.Setters> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
還要把預設的gridMain的兩列的寬度預設值分別改為*和0:
<Grid x:Name="gridMain" > <Grid.RenderTransform> <CompositeTransform /> </Grid.RenderTransform> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="0" /> </Grid.ColumnDefinitions> <Frame x:Name="masterFrame" Grid.Column="0" mvvm:StageManager.Beacon="masterFrame" x:FieldModifier="public"/> <Frame x:Name="detailFrame" Content="{Binding DetailContent,Mode=TwoWay}" HorizontalAlignment="Stretch" Grid.Column="1" mvvm:StageManager.Beacon="detailFrame" x:FieldModifier="public"> </Frame> </Grid>
Trigger的意義很清楚了,Setter會根據不同的狀態去設置gridMain兩列的寬度來控制MasterPage和DetailPage的顯示和隱藏:
當剛開始進入程式,左側顯示列表,右側顯示BlankPage,這時候如果寬度大於720,兩個頁面正常展示,如果頁面寬度小於720,則只顯示列表頁;
如果頁面寬度大於720的時候,點擊列表,右側正常顯示詳情;
如果頁面寬度小於720,點擊列表,列表會隱藏,只顯示詳情;
基本達到了文章開頭提出的目的。
四、處理返回鍵
當在手機上運行的時候,就會發現當點擊列表顯示DetailPage後,再按返回鍵直接退出程式了。因為還沒有處理返回鍵事件。PC上也一樣,程式左上角應該有個返回按鈕。下麵來處理返回事件。
基本思路是,點擊返回後,應該先判斷DetailPage是否可GoBack,如果可以就GoBack,直到返回最開始的BlankPage為止,這樣StateTrigger會自動觸發NarrowAndBlankDetail狀態,顯示MasterPage。
返回是處理SystemNavigationManager.GetForCurrentView().BackRequested這個事件,打開MainPage.xaml.cs文件,在OnNavigatedTo里訂閱這個事件:
protected override void OnNavigatedTo(NavigationEventArgs e) { SystemNavigationManager.GetForCurrentView().BackRequested += CurrentView_BackRequested; //SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible; base.OnNavigatedTo(e); } protected override void OnNavigatedFrom(NavigationEventArgs e) { SystemNavigationManager.GetForCurrentView().BackRequested -= CurrentView_BackRequested; base.OnNavigatedFrom(e); } private void CurrentView_BackRequested(object sender, BackRequestedEventArgs e) { //判斷DetailPage能否GoBack,如果可以GoBack則GoBack 顯示BlankPage //其次判斷MasterPage能否GoBack,如果可以GoBack則GoBack //如果不能GoBack,則提示是否退出 if (StrongTypeViewModel.StageManager["detailFrame"].CanGoBack) { e.Handled = true; StrongTypeViewModel.StageManager["detailFrame"].Frame.GoBack(); } else if (StrongTypeViewModel.StageManager["masterFrame"].CanGoBack) { e.Handled = true; StrongTypeViewModel.StageManager["masterFrame"].Frame.GoBack(); } else { //TODO 隱藏回退鍵 提示退出 SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Collapsed; } }
用戶點擊返回鍵的時候,首先看DetailPage能否GoBack,再看MasterPage能否GoBack,當沒有可GoBack的時候就把返回鍵隱藏。
在PC上的返回鍵預設是隱藏的,還需要在導航到詳情頁的時候將其展示出來,修改MainPage_Model.cs文件里的RegisterCommand方法:
private void RegisterCommand() { MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>() .Where(x => x.EventName == "NewsItemTapped") .Subscribe( async e => { NewsItem item = e.EventData as NewsItem; SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible; await StageManager["detailFrame"].Show(new DetailPage_Model(item)); } ).DisposeWith(this); }
現在運行一下,PC上也可以返回了。當第一次打開的時候,是這樣 的:
如果拖動視窗縮小,則只會顯示MasterPage:
當點擊列表項時,會只顯示DetailPage:
點擊左上角返回鍵,又只顯示MasterPage了。
具體切換動畫我不會截圖,大家可以下載demo自己試試。
五、添加切換動畫效果
我們還可以做的更美觀一點。UWP預設的Page切換是有動畫效果的,但這裡因為只使用StateTrigger設置了Grid的列寬,當從DetailPage返回MasterPage的時候MasterPage一下子就顯示出來了,感覺有點生硬。現在給切換加一個動畫。
在NarrowAndBlankDetail的VisualState里,添加一段StoryBoard:
<Storyboard > <DoubleAnimation Storyboard.TargetName="gridMain" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.6"> <DoubleAnimation.EasingFunction> <CircleEase EasingMode="EaseOut" /> </DoubleAnimation.EasingFunction> </DoubleAnimation> <DoubleAnimation Storyboard.TargetName="gridMain" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" From="-100" To="0" Duration="0:0:0.3"> <DoubleAnimation.EasingFunction> <CircleEase EasingMode="EaseOut" /> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard>
設置透明度從0到1,同時有一個移動的效果。註意這裡的StoryBoard.TargetProperty的寫法,詳細說明可以參考MSDN文檔:
https://msdn.microsoft.com/zh-cn/library/windows/apps/jj569302.aspx
再次吐槽一下MSDN文檔真是太難找了。版本太多。
在<VisualStateManager.VisualStateGroups>里添加Transitions:
<VisualStateGroup.Transitions> <VisualTransition From="NarrowAndNoBlankDetail" To="NarrowAndBlankDetail" ></VisualTransition> </VisualStateGroup.Transitions>
同時要在gridMain里添加以下代碼:
<Grid.RenderTransform> <CompositeTransform /> </Grid.RenderTransfor