WPF Custom Controls Part 11 scrollable TabControl B

14 בMay 2014

אין תגובות

ננסה לבנות פקד שמכיל את הלוגיקה שעוטפת את הטאבים ויודע לגלול ביניהם.

יכולות להיות לנו שלש לוגיקות בסיסיות:

1.לחשוף Dependency Property שאיתו נוכל לאפיין מה רוחב כל טאב, ועפ”י זה לגלול. זה אמור להיות הפתרון הקל ביותר,

2. לקבל מתוך העץ הויזואלי את הרוחב הנדרש, נניח של כל טאב

כאן הזהירות היא חובה , כי נצטרך להניח שאכן יש לנו טאבים והמבנה הויזואלי הוא נורמלי,

3. לקבל מתוך העץ הויזואלי את הרוחב של טאב כלשהו בהנחה שכל הטאבים זהים ברוחב ה Tab Header

שלהם ואז לגלול בהתאם.

בכל מקרה אחת הבעיות המרכזיות שנצטרך לתמוך בה (או להתעלם) היא בשינוי גודל החלון תוך כדי עבודה,

זה מאוד הגיוני לתמוך בזה, ונסביר: ישנו גודל התחלתי של חלון בעליית אפליקציה ואם זה נוח לי להתמודד מצד שני, אם עשו resize לחלון בזמן ריצה כדאי מאוג לתמוך גם אז.

אז מימוש יהיה כך,

א. נפתח פרוייקט custom Control ניצור קלאס ונגדיר אותו כיורש של TabControl.

ב. נגדיר DependencyProperty עבור טווח הגלילה בכל לחיצה,

ג. נארגן איזשהו מימוש מוכן של command פנימי לצרכי הפעלת הכפתורים.

ד. נטפל בלוגיקת הגלילה ימינה ושמאלה באמצעות 2 מופעים של Commands גם הם בצורת Properties.

הנה הקוד שעושה את כל זה :

   1: public class ScrollableTab : TabControl

   2:     {

   3:         /// counts the clicks per each side of the tabs container

   4:         protected static int ClickCounter = 0;

   5:

   6:         static ScrollableTab()

   7:         {

   8:             DefaultStyleKeyProperty.OverrideMetadata(typeof(ScrollableTab), new FrameworkPropertyMetadata(typeof(ScrollableTab)));

   9:         }

  10:         public ScrollableTab()

  11:         {

  12:             ScroToLeftOffsetCommand = new MyCommand<ScrollViewer>(scrollToLeft);

  13:             ScrollToRightOffsetCommand = new MyCommand<ScrollViewer>(scrollToRight);

  14:             Loaded += ScrollableTab_Loaded;

  15:         }

  16:

  17:         void ScrollableTab_Loaded(object sender, RoutedEventArgs e)

  18:         {

  19:             //no value sets in xaml customer of this control

  20:             if (ScrollOffset == default(double))

  21:             {

  22:                 if (this.Items.Count > 0)

  23:                 {

  24:                     TabItem tb = (TabItem)this.Items[0];

  25:                     ScrollOffset = tb.ActualWidth;

  26:                 }

  27:             }

  28:         }

  29:

  30:         public double ScrollOffset

  31:         {

  32:             get { return (double)GetValue(ScrollOffsetProperty); }

  33:             set { SetValue(ScrollOffsetProperty, value); }

  34:         }

  35:

  36:         public static readonly DependencyProperty ScrollOffsetProperty =

  37:             DependencyProperty.Register("ScrollOffset", typeof(double), typeof(ScrollableTab), new PropertyMetadata(0.0, onDependecyPropertyValueChanged));

  38:

  39:         private static void onDependecyPropertyValueChanged(object sender, DependencyPropertyChangedEventArgs args)

  40:         {

  41:             ScrollableTab s_tab = sender as ScrollableTab;

  42:             s_tab.ScrollOffset = (double)args.NewValue;

  43:         }

  44:

  45:         #region control internal commands

  46:         /// this is the Scroll to left Tab implementation, which is depend on internal value of tab width

  47:         /// 

  48:         //scroll to left 

  49:         public MyCommand<ScrollViewer> ScroToLeftOffsetCommand

  50:         {

  51:             get { return (MyCommand<ScrollViewer>)GetValue(ScroToLeftOffsetCommandProperty); }

  52:             set { SetValue(ScroToLeftOffsetCommandProperty, value); }

  53:         }

  54:

  55:         public static readonly DependencyProperty ScroToLeftOffsetCommandProperty =

  56:             DependencyProperty.Register("ScroToLeftOffsetCommand", typeof(MyCommand<ScrollViewer>), typeof(ScrollableTab), new PropertyMetadata(default(MyCommand<ScrollViewer>)));

  57:

  58:         private void scrollToLeft(ScrollViewer UIScrollViewer)

  59:         {

  60:             var allTabsWidth = this.Items.Count * ScrollOffset; //the sum of all available tabs in pxls

  61:             var viewPortTabs = allTabsWidth - this.ActualWidth - 40; // the sum of the visible tubs minus 40 extra for buttons in pxls

  62:             var richToEndOfTabs = (ClickCounter * ScrollOffset) - 40; //the aprox distance from here to the end of tabs minus 40 extra for buttons in pxls

  63:

  64:             if (richToEndOfTabs <= viewPortTabs)

  65:             {

  66:                 ClickCounter++;

  67:                 UIScrollViewer.ScrollToHorizontalOffset(ScrollOffset * ClickCounter);

  68:             }

  69:

  70:         }

  71:

  72:         //scroll to right

  73:

  74:         public MyCommand<ScrollViewer> ScrollToRightOffsetCommand

  75:         {

  76:             get { return (MyCommand<ScrollViewer>)GetValue(ScrollToRightOffsetCommandProperty); }

  77:             set { SetValue(ScrollToRightOffsetCommandProperty, value); }

  78:         }

  79:

  80:         public static readonly DependencyProperty ScrollToRightOffsetCommandProperty =

  81:             DependencyProperty.Register("ScrollToRightOffsetCommand", typeof(MyCommand<ScrollViewer>), typeof(ScrollableTab), new PropertyMetadata(default(MyCommand<ScrollViewer>)));

  82:

  83:

  84:         private void scrollToRight(ScrollViewer UIScrollViewer)

  85:         {

  86:             //check when rich to left end of items

  87:             if (ClickCounter > 0)

  88:             {

  89:                 ClickCounter--;

  90:                 UIScrollViewer.ScrollToHorizontalOffset((ScrollOffset) * ClickCounter);

  91:             }

  92:         }

  93:         #endregion

  94:

  95:

  96:         public class MyCommand<ScrollViewer> : ICommand

  97:         {

  98:             private Action<ScrollViewer> _execute;

  99:

 100:             public MyCommand(Action<ScrollViewer> execute)

 101:             {

 102:                 _execute = execute;

 103:             }

 104:

 105:             public bool CanExecute(object parameter)

 106:             {

 107:                 return true;

 108:             }

 109:

 110:             public event EventHandler CanExecuteChanged;

 111:

 112:             public void Execute(object parameter)

 113:             {

 114:                 if (_execute != null)

 115:                     _execute((ScrollViewer)parameter);

 116:             }

 117:

 118:

 119:         }

 120:

 121:                                                                                 }

זה כולל כבר אינסטנס בסיסי ביותר של command, ורק נשים לב ללוגיקה של הפונקציה scrollToLeft,

זהו בעצם האיזור הכי מורכב בקוד שכן את יכולת הגלילה שלי אני בונה על:

א. סכימת כמות הטאבים הכללית כפול רוחב טאב והתוצאה היא הרוחב הנדרש לכל הטאבים

ב. מציאת סך הטאבים הנגלה לעין לפי הנוסחה של רוחב הפקד ביחס לנתון הקודם

בכל סיבוב אני בודק אם לאפשר “גלילה” בהתאם להפרש בין שני נתונים אלה.

בכל מקרה ,הלוגיקה המרכזית פה היא שימוש בפונקציה המובנית ScrollToHorizontalOffset של ScrollViewer שבעצם גוללת לנקודה על ציר ה X של הפקד, והנתון שאני מספק לפונקציה הוא מספר הלחיצות של המשתמש כפול רוחב טאב,

את מספר הלחיצות אנחנו דואגים לסנכרן למעלה ולמטה בכל לחיצה ימינה או שמאלה.

 

ניגש לUI, לא ניכנס ליותר מדי פרטים וזה גם לא רלוונטי, אני אמור לשתול בתוך הtemplate של הפקד כפתורי גלילה ועטיפה של ScrollViewer. מי שיודע לקרוא xaml יראה מייד איך זה עובד, ובסופו של דבר מה שחשוב זה החיבור לcommands של הקוד הלוגי, תוך העברת commandParameter ז\ששם אנחנו שולפים את הScrollViewer מתוך הtemplate שבנינו ומעבירים דרך Binding לקוד הלוגי.

אני באופן שרירותי קבעתי את רוחב כפתורי הגלילה ל px20 ולכן גם בלוגיקה התייחסתי לזה בהפחתת px40

מהגודל הנראה של סך הטאבים.

לא נלאה בדיבורים ואני גם לא גרפיקאי גדול אבל זאת ספריית הxaml שתפעיל את הפקד:

   1: <ResourceDictionary

   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   4:     xmlns:local="clr-namespace:scrolbableTabcontrol">

   5:  

   6:     <SolidColorBrush x:Key="NormalBrush" Color="#FFCCCCCC"/>

   7:  

   8:     <SolidColorBrush x:Key="PressedBrush" Color="#FFEEEEEE"/>

   9:  

  10:     <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />

  11:  

  12:     <!-- Border Brushes -->

  13:     <LinearGradientBrush x:Key="NormalBorderBrush" StartPoint="0,0" EndPoint="0,1">

  14:         <GradientBrush.GradientStops>

  15:             <GradientStopCollection>

  16:                 <GradientStop Color="#CCC" Offset="0.0"/>

  17:                 <GradientStop Color="#444" Offset="1.0"/>

  18:             </GradientStopCollection>

  19:         </GradientBrush.GradientStops>

  20:     </LinearGradientBrush>

  21:  

  22:     <Style x:Key="ScrollBarLineButton" TargetType="{x:Type RepeatButton}">

  23:         <Setter Property="SnapsToDevicePixels" Value="True"/>

  24:         <Setter Property="OverridesDefaultStyle" Value="true"/>

  25:         <Setter Property="Focusable" Value="false"/>

  26:         <Setter Property="Template">

  27:             <Setter.Value>

  28:                 <ControlTemplate TargetType="{x:Type RepeatButton}">

  29:                     <Border Name="Border" CornerRadius="2"  Background="{StaticResource NormalBrush}"

  30:                               BorderBrush="{StaticResource NormalBorderBrush}" >

  31:                         <Path HorizontalAlignment="Center" VerticalAlignment="Center" Fill="Gray"

  32:                                 Data="{Binding Path=Content, RelativeSource={RelativeSource TemplatedParent}}" 

  33:                                 Opacity="{Binding Path=Opacity, RelativeSource={RelativeSource TemplatedParent}}"  />

  34:                     </Border>

  35:                     <ControlTemplate.Triggers>

  36:                         <Trigger Property="IsPressed" Value="true">

  37:                             <Setter TargetName="Border" Property="Background" Value="{StaticResource PressedBrush}" />

  38:                         </Trigger>

  39:                         <Trigger Property="IsEnabled" Value="false">

  40:                             <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}"/>

  41:                         </Trigger>

  42:                     </ControlTemplate.Triggers>

  43:                 </ControlTemplate>

  44:             </Setter.Value>

  45:         </Setter>

  46:     </Style>

  47:  

  48:     <Style TargetType="{x:Type local:ScrollableTab}">

  49:         <Setter Property="SnapsToDevicePixels" Value="True" />

  50:          

  51:         <Setter Property="Template">

  52:             <Setter.Value>

  53:                 <ControlTemplate TargetType="{x:Type local:ScrollableTab}">

  54:                     <Grid x:Name="Panel" Margin="0,2" >

  55:                             <Grid.ColumnDefinitions>

  56:                                 <ColumnDefinition Width="Auto"/>

  57:                                 <ColumnDefinition Width="*"/>

  58:                                 <ColumnDefinition Width="Auto"/>

  59:                             </Grid.ColumnDefinitions>

  60:                         <RepeatButton x:Name="LineLeftButton" Grid.Column="0" Width="20" Height="30" Visibility="Visible" Style="{StaticResource ScrollBarLineButton}"

  61:                                       Content="M 8 0 L 8 32 L 0 16 Z"       

  62:                                       Command="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ScrollToRightOffsetCommand}" CommandParameter="{Binding ElementName=scrollviewer}"

  63:                             CommandTarget="{Binding ElementName=scrollviewer}"

  64:                                       ClickMode="Press" />

  65:                         <RepeatButton x:Name="LineRightButton" Grid.Column="2" Width="20" Height="30"  Visibility="Visible" Style="{StaticResource ScrollBarLineButton}"

  66:                                       Content="M 0 0 L 8 16 L 0 32 Z" 

  67:                                        Command="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ScroToLeftOffsetCommand}" CommandParameter="{Binding ElementName=scrollviewer}"  

  68:                                       CommandTarget="{Binding ElementName=scrollviewer}"

  69:                                       ClickMode="Press"/>

  70:                      

  71:                             <ScrollViewer x:Name="scrollviewer" CanContentScroll="True" Grid.Column="1" VerticalAlignment="Top" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">

  72:                         <ScrollViewer.Template>

  73:                             <ControlTemplate TargetType="{x:Type ScrollViewer}" >

  74:                                 <Grid>

  75:                                     <ScrollBar x:Name="PART_HorizontalScrollBar" Orientation="Horizontal" 

  76:                                                                Value="{TemplateBinding HorizontalOffset}"

  77:                                                                Maximum="{TemplateBinding ScrollableWidth}"

  78:                                                                ViewportSize="{TemplateBinding ViewportWidth}"

  79:                                                                Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"

  80:                                                                Height="{Binding Height, ElementName=Panel}">

  81:                                         <ScrollBar.Template>

  82:                                             <ControlTemplate>

  83:                                                 <Track x:Name="PART_Track">

  84:                                                     <Track.DecreaseRepeatButton>

  85:                                                         <RepeatButton Command="ScrollBar.PageLeftCommand" />

  86:                                                     </Track.DecreaseRepeatButton>

  87:                                                     <Track.IncreaseRepeatButton>

  88:                                                         <RepeatButton Command="ScrollBar.PageRightCommand"/>

  89:                                                     </Track.IncreaseRepeatButton>

  90:                                                     <Track.Thumb>

  91:                                                         <Thumb  

  92:                                                                          Background="Gray" Opacity="0.8" Margin="0,-1" />

  93:                                                     </Track.Thumb>

  94:                                                 </Track>

  95:                                             </ControlTemplate>

  96:                                         </ScrollBar.Template>

  97:                                     </ScrollBar>

  98:                                     <ScrollContentPresenter Margin="0,2" Height="Auto" VerticalAlignment="Center"/>

  99:                                 </Grid>

 100:                             </ControlTemplate>

 101:                         </ScrollViewer.Template>

 102:                  

 103:                     <Grid KeyboardNavigation.TabNavigation="Local">

 104:                         <Grid.RowDefinitions>

 105:                             <RowDefinition Height="Auto" />

 106:                             <RowDefinition Height="*" />

 107:                         </Grid.RowDefinitions>

 108:                         <VisualStateManager.VisualStateGroups>

 109:                             <VisualStateGroup x:Name="CommonStates">

 110:                                 <VisualState x:Name="Disabled">

 111:                                     <Storyboard>

 112:                                         <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"

 113:                                                 Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)">

 114:                                             <EasingColorKeyFrame KeyTime="0"

 115:                                          Value="#FFAAAAAA" />

 116:                                         </ColorAnimationUsingKeyFrames>

 117:                                     </Storyboard>

 118:                                 </VisualState>

 119:                             </VisualStateGroup>

 120:                         </VisualStateManager.VisualStateGroups>

 121:                                 <TabPanel x:Name="HeaderPanel"

 122:                     Grid.Row="0"

 123:                     Panel.ZIndex="1"

 124:                     Margin="0,0,4,-1"

 125:                     IsItemsHost="True"

 126:                     KeyboardNavigation.TabIndex="1"

 127:                     Background="Transparent"/>

 128:                  <Border x:Name="Border"

 129:                   Grid.Row="1"

 130:                   BorderThickness="1"

 131:                   CornerRadius="2"

 132:                   KeyboardNavigation.TabNavigation="Local"

 133:                   KeyboardNavigation.DirectionalNavigation="Contained"

 134:                   KeyboardNavigation.TabIndex="2">

 135:                             <Border.Background>

 136:                                 <LinearGradientBrush EndPoint="0.5,1"

 137:                                    StartPoint="0.5,0">

 138:                                     <GradientStop Color="{DynamicResource ContentAreaColorLight}"

 139:                               Offset="0" />

 140:                                     <GradientStop Color="{DynamicResource ContentAreaColorDark}"

 141:                               Offset="1" />

 142:                                 </LinearGradientBrush>

 143:                             </Border.Background>

 144:                             <Border.BorderBrush>

 145:                                 <SolidColorBrush Color="{DynamicResource BorderMediumColor}"/>

 146:                             </Border.BorderBrush>

 147:                             <ContentPresenter x:Name="PART_SelectedContentHost"

 148:                               Margin="4"

 149:                               ContentSource="SelectedContent" />

 150:                         </Border>

 151:                             </Grid>

 152:                         </ScrollViewer>

 153:                     </Grid>

 154:                 </ControlTemplate>

 155:             </Setter.Value>

 156:         </Setter>

 157:     </Style>

 158:   

 159: </ResourceDictionary>

 160:  

*הערת סיום, זוהי דוגמא טובה לאיך מתחילים לתכנן פקדים מתקדמים, זה לא קוד מושלם עדיין,

(מן הסתם אם היה מושלם הייתי לוקח תשלום עליו…)

הוסף תגובה
facebook linkedin twitter email

Leave a Reply

Your email address will not be published. Required fields are marked *