WPF Custom Controls Part 5 – custom Scroll Bar

14 בMay 2013

תגיות: , ,
אין תגובות
נתחיל בקריאה של הפוסט הקודם שלי כאן. לא כי אני מפרסם את עצמי.. אני מתבסס על ידע שמוצג שם. מה שנרצה לעשות הפעם זה “ללכת עם אבל להרגיש בלי”
ננסה להגדיר ScrollBar משלנו ונגדיר קודם מה המטרה שלי. אני רוצה LineScrollBar שזה אומר , שיגלול תוכן אבל יידע להתיישר לתחילת שורה.כדי להבין נבדוק את הפוסט הקודם ונראה איך מתנהג ScrollViewer עם הפרופרטי CanContentScroll = true , ונראה גם את תחושת הגלילה החלקה כאילו CanContentScroll = false נסביר מה הכוונה,
 
רוב הscrollbars מתנהגים באופן חפשי אני גולל והקונטנט זז למעלה ולמטה, גם במחיר שחצי מהשורה הראשונה(לדוגמא) נסתר מעין הקורא. (לדוגמא בדפדפנים), או בתכנת עיבוד תמלילים (וואו איזה שם היסטורי, word לדוגמא) אבל אנחנו נגדיר את הדרישות כך:
אפקט הגלילה של הthumb יעבוד חלק, בזמן גלילה(כשהעכבר לחוץ) נחווה גלילה חפשית (כמו בדפדפן) ובוודאי אם נרצה להתאים אותו להתקן עם מסך מגע. אבל ברגע שנשחרר את פעולת הגלילה (עכבר, מקלדת, טאצ’) נקבל יישור קוו של לחצן הגלילה (thumb, כך הוא נקרא) והוא יתיישר אוטומטית לשורה הקרובה ביותר מלמעלה או למטה(כשהמשמעות העיקרית ברמת התצוגה). נעשה את זה שלב שלב.
 
 לפרוייקט שלנו נוסיף תיקייה בשם controls ובתוכה נוסיף Custom Controll שיורש מScrollBar. כזכור יש לנו שני קבצים שצריך לטפל בהם. האחד זה הstyle והשני זה הקובץ הלוגי שמתאר את לוגיקת הפקד עצמו קודם ניפטר מעיצוב הUI שלנו, זאת לא משימה קלה מדובר בפקד מורכב לכן את הקוד המופיע מיד או תעתיקו או תחפשו לו מקבילה בגוגל. אבל משימה אחת לפחות היא חובה וזה הוספת רפרנס לפרויקט לDLL של PresentationFramework.Aero מדובר בספריית הסטייל הבסיסית של מערכת ההפעלה ואנו נשתמש בה על הדרך. אחרי ההוספה נדרשת השורה הבאה בקובץ הdictionary Xaml של:
   1: xmlns:Microsoft_Windows_Themes="clr-                                        

   2: pace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"   

אני ממש לא נכנס לתחום הstyle של הקונטרול נסו להעתיק את כל הבלוק הבא לgeneric.xaml שלכם: או פשוט להעתיק את זה דרך MSDN..
   1: <LinearGradientBrush x:Key="NormalBrush" StartPoint="0,0" EndPoint="0,1">

   2:         <GradientBrush.GradientStops>

   3:             <GradientStopCollection>

   4:                 <GradientStop Color="#FFF" Offset="0.0"/>

   5:                 <GradientStop Color="#CCC" Offset="1.0"/>

   6:             </GradientStopCollection>

   7:         </GradientBrush.GradientStops>

   8:     </LinearGradientBrush>

   9:  

  10:     <LinearGradientBrush x:Key="HorizontalNormalBrush" StartPoint="0,0" EndPoint="1,0">

  11:         <GradientBrush.GradientStops>

  12:             <GradientStopCollection>

  13:                 <GradientStop Color="#FFF" Offset="0.0"/>

  14:                 <GradientStop Color="#CCC" Offset="1.0"/>

  15:             </GradientStopCollection>

  16:         </GradientBrush.GradientStops>

  17:     </LinearGradientBrush>

  18:  

  19:     <LinearGradientBrush x:Key="LightBrush" StartPoint="0,0" EndPoint="0,1">

  20:         <GradientBrush.GradientStops>

  21:             <GradientStopCollection>

  22:                 <GradientStop Color="#FFF" Offset="0.0"/>

  23:                 <GradientStop Color="#EEE" Offset="1.0"/>

  24:             </GradientStopCollection>

  25:         </GradientBrush.GradientStops>

  26:     </LinearGradientBrush>

  27:  

  28:     <LinearGradientBrush x:Key="HorizontalLightBrush" StartPoint="0,0" EndPoint="1,0">

  29:         <GradientBrush.GradientStops>

  30:             <GradientStopCollection>

  31:                 <GradientStop Color="#FFF" Offset="0.0"/>

  32:                 <GradientStop Color="#EEE" Offset="1.0"/>

  33:             </GradientStopCollection>

  34:         </GradientBrush.GradientStops>

  35:     </LinearGradientBrush>

  36:  

  37:     <LinearGradientBrush x:Key="DarkBrush" StartPoint="0,0" EndPoint="0,1">

  38:         <GradientBrush.GradientStops>

  39:             <GradientStopCollection>

  40:                 <GradientStop Color="#FFF" Offset="0.0"/>

  41:                 <GradientStop Color="#AAA" Offset="1.0"/>

  42:             </GradientStopCollection>

  43:         </GradientBrush.GradientStops>

  44:     </LinearGradientBrush>

  45:  

  46:     <LinearGradientBrush x:Key="PressedBrush" StartPoint="0,0" EndPoint="0,1">

  47:         <GradientBrush.GradientStops>

  48:             <GradientStopCollection>

  49:                 <GradientStop Color="#BBB" Offset="0.0"/>

  50:                 <GradientStop Color="#EEE" Offset="0.1"/>

  51:                 <GradientStop Color="#EEE" Offset="0.9"/>

  52:                 <GradientStop Color="#FFF" Offset="1.0"/>

  53:             </GradientStopCollection>

  54:         </GradientBrush.GradientStops>

  55:     </LinearGradientBrush>

  56:  

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

  58:  

  59:     <SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" />

  60:  

  61:     <SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" />

  62:  

  63:     <SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#DDD" />

  64:  

  65:     <!-- Border Brushes -->

  66:  

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

  68:         <GradientBrush.GradientStops>

  69:             <GradientStopCollection>

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

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

  72:             </GradientStopCollection>

  73:         </GradientBrush.GradientStops>

  74:     </LinearGradientBrush>

  75:  

  76:     <LinearGradientBrush x:Key="HorizontalNormalBorderBrush" StartPoint="0,0" EndPoint="1,0">

  77:         <GradientBrush.GradientStops>

  78:             <GradientStopCollection>

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

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

  81:             </GradientStopCollection>

  82:         </GradientBrush.GradientStops>

  83:     </LinearGradientBrush>

  84:  

  85:     <LinearGradientBrush x:Key="DefaultedBorderBrush" StartPoint="0,0" EndPoint="0,1">

  86:         <GradientBrush.GradientStops>

  87:             <GradientStopCollection>

  88:                 <GradientStop Color="#777" Offset="0.0"/>

  89:                 <GradientStop Color="#000" Offset="1.0"/>

  90:             </GradientStopCollection>

  91:         </GradientBrush.GradientStops>

  92:     </LinearGradientBrush>

  93:  

  94:     <LinearGradientBrush x:Key="PressedBorderBrush" StartPoint="0,0" EndPoint="0,1">

  95:         <GradientBrush.GradientStops>

  96:             <GradientStopCollection>

  97:                 <GradientStop Color="#444" Offset="0.0"/>

  98:                 <GradientStop Color="#888" Offset="1.0"/>

  99:             </GradientStopCollection>

 100:         </GradientBrush.GradientStops>

 101:     </LinearGradientBrush>

 102:  

 103:     <SolidColorBrush x:Key="DisabledBorderBrush" Color="#AAA" />

 104:  

 105:     <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />

 106:  

 107:     <SolidColorBrush x:Key="LightBorderBrush" Color="#AAA" />

 108:  

 109:     <!-- Miscellaneous Brushes -->

 110:     <SolidColorBrush x:Key="GlyphBrush" Color="#444" />

 111:  

 112:     <SolidColorBrush x:Key="LightColorBrush" Color="#DDD" />

 113:  

 114:  

 115:  

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

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

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

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

 120:         <Setter Property="Template">

 121:             <Setter.Value>

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

 123:                     <Border 

 124:           Name="Border"

 125:           Margin="1" 

 126:           CornerRadius="2" 

 127:           Background="{StaticResource NormalBrush}"

 128:           BorderBrush="{StaticResource NormalBorderBrush}"

 129:           BorderThickness="1">

 130:                         <Path 

 131:             HorizontalAlignment="Center"

 132:             VerticalAlignment="Center"

 133:             Fill="{StaticResource GlyphBrush}"

 134:             Data="{Binding Path=Content,RelativeSource={RelativeSource TemplatedParent}}" />

 135:                     </Border>

 136:                     <ControlTemplate.Triggers>

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

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

 139:                         </Trigger>

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

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

 142:                         </Trigger>

 143:                     </ControlTemplate.Triggers>

 144:                 </ControlTemplate>

 145:             </Setter.Value>

 146:         </Setter>

 147:     </Style>

 148:  

 149:     <Style x:Key="ScrollBarPageButton" TargetType="{x:Type RepeatButton}">

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

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

 152:         <Setter Property="IsTabStop" Value="false"/>

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

 154:         <Setter Property="Template">

 155:             <Setter.Value>

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

 157:                     <Border Background="Transparent" />

 158:                 </ControlTemplate>

 159:             </Setter.Value>

 160:         </Setter>

 161:     </Style>

 162:  

 163:     <Style x:Key="ScrollBarThumb" TargetType="{x:Type Thumb}">

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

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

 166:         <Setter Property="IsTabStop" Value="false"/>

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

 168:         <Setter Property="Height" Value="50" />

 169:         <Setter Property="Template">

 170:             <Setter.Value>

 171:                 <ControlTemplate TargetType="{x:Type Thumb}">

 172:                     <Border 

 173:           CornerRadius="2" 

 174:           Background="{TemplateBinding Background}"

 175:           BorderBrush="{TemplateBinding BorderBrush}"

 176:           BorderThickness="1" />

 177:                 </ControlTemplate>

 178:             </Setter.Value>

 179:         </Setter>

 180:     </Style>

 181:  

 182:     <ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">

 183:         <Grid >

 184:             <Grid.RowDefinitions>

 185:                 <RowDefinition MaxHeight="18"/>

 186:                 <RowDefinition Height="0.00001*"/>

 187:                 <RowDefinition MaxHeight="18"/>

 188:             </Grid.RowDefinitions>

 189:             <Border

 190:       Grid.RowSpan="3"

 191:       CornerRadius="2" 

 192:       Background="#F0F0F0" />

 193:             <RepeatButton 

 194:       Grid.Row="0"                           

 195:       Style="{StaticResource ScrollBarLineButton}"

 196:       Height="18"

 197:       Command="ScrollBar.LineUpCommand"

 198:       Content="M 0 4 L 8 4 L 4 0 Z" />

 199:             <Track 

 200:       Name="PART_Track"

 201:       Grid.Row="1"

 202:       IsDirectionReversed="true">

 203:                 <Track.DecreaseRepeatButton>

 204:                     <RepeatButton 

 205:           Style="{StaticResource ScrollBarPageButton}"

 206:           Command="ScrollBar.PageUpCommand" />

 207:                 </Track.DecreaseRepeatButton>

 208:                 <Track.Thumb>

 209:                     <Thumb 

 210:           Style="{StaticResource ScrollBarThumb}" 

 211:           Margin="1,0,1,0"  

 212:           Background="{StaticResource HorizontalNormalBrush}"

 213:           BorderBrush="{StaticResource HorizontalNormalBorderBrush}" />

 214:                 </Track.Thumb>

 215:                 <Track.IncreaseRepeatButton>

 216:                     <RepeatButton 

 217:           Style="{StaticResource ScrollBarPageButton}"

 218:           Command="ScrollBar.PageDownCommand" />

 219:                 </Track.IncreaseRepeatButton>

 220:             </Track>

 221:             <RepeatButton 

 222:       Grid.Row="3" 

 223:       Style="{StaticResource ScrollBarLineButton}"

 224:       Height="18"

 225:       Command="ScrollBar.LineDownCommand"

 226:       Content="M 0 0 L 4 4 L 8 0 Z"/>

 227:         </Grid>

 228:     </ControlTemplate>

 229:  

 230:     <ControlTemplate x:Key="HorizontalScrollBar" TargetType="{x:Type ScrollBar}">

 231:         <Grid >

 232:             <Grid.ColumnDefinitions>

 233:                 <ColumnDefinition MaxWidth="18"/>

 234:                 <ColumnDefinition Width="0.00001*"/>

 235:                 <ColumnDefinition MaxWidth="18"/>

 236:             </Grid.ColumnDefinitions>

 237:             <Border

 238:       Grid.ColumnSpan="3"

 239:       CornerRadius="2" 

 240:       Background="#F0F0F0" />

 241:             <RepeatButton 

 242:       Grid.Column="0"                           

 243:       Style="{StaticResource ScrollBarLineButton}"

 244:       Width="18"

 245:       Command="ScrollBar.LineLeftCommand"

 246:       Content="M 4 0 L 4 8 L 0 4 Z" />

 247:             <Track 

 248:       Name="PART_Track"

 249:       Grid.Column="1"

 250:       IsDirectionReversed="False">

 251:                 <Track.DecreaseRepeatButton>

 252:                     <RepeatButton 

 253:           Style="{StaticResource ScrollBarPageButton}"

 254:           Command="ScrollBar.PageLeftCommand" />

 255:                 </Track.DecreaseRepeatButton>

 256:                 <Track.Thumb>

 257:                     <Thumb 

 258:           Style="{StaticResource ScrollBarThumb}" 

 259:           Margin="0,1,0,1"  

 260:           Background="{StaticResource NormalBrush}"

 261:           BorderBrush="{StaticResource NormalBorderBrush}" />

 262:                 </Track.Thumb>

 263:                 <Track.IncreaseRepeatButton>

 264:                     <RepeatButton 

 265:           Style="{StaticResource ScrollBarPageButton}"

 266:           Command="ScrollBar.PageRightCommand" />

 267:                 </Track.IncreaseRepeatButton>

 268:             </Track>

 269:             <RepeatButton 

 270:       Grid.Column="3" 

 271:       Style="{StaticResource ScrollBarLineButton}"

 272:       Width="18"

 273:       Command="ScrollBar.LineRightCommand"

 274:       Content="M 0 0 L 4 4 L 0 8 Z"/>

 275:         </Grid>

 276:     </ControlTemplate>

 277:  

 278:     <Style x:Key="{x:Type local:CustomScrollBar}" TargetType="{x:Type local:CustomScrollBar}">

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

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

 281:         <Style.Triggers>

 282:             <Trigger Property="Orientation" Value="Horizontal">

 283:                 <Setter Property="Width" Value="Auto"/>

 284:                 <Setter Property="Height" Value="18" />

 285:                 <Setter Property="Template" Value="{StaticResource HorizontalScrollBar}" />

 286:             </Trigger>

 287:             <Trigger Property="Orientation" Value="Vertical">

 288:                 <Setter Property="Width" Value="18"/>

 289:                 <Setter Property="Height" Value="Auto" />

 290:                 <Setter Property="Template" Value="{StaticResource VerticalScrollBar}" />

 291:             </Trigger>

 292:         </Style.Triggers>

 293:     </Style>

מתנצל על הגועל.. אבל נחזור לתכל’ס. קוד הC# שלי נראה כך:
   1: public class CustomScrollBar : System.Windows.Controls.Primitives.ScrollBar

   2:     {

   3:         static CustomScrollBar()

   4:         {

   5:             DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomScrollBar), new FrameworkPropertyMetadata(typeof(CustomScrollBar)));

   6:         }

   7:  

   8:         public CustomScrollBar()

   9:         {

  10:         }

  11:         

  12:         private double _newValue;

  13:         private double _oldValue;

  14:         public bool feedBackOffset = false;

  15:    

  16:         public double ItemHeigt

  17:         {

  18:             get { return (double)GetValue(ItemHeigtProperty); }

  19:             set { SetValue(ItemHeigtProperty, value); }

  20:         }

  21:  

  22:         public static readonly DependencyProperty ItemHeigtProperty =

  23:             DependencyProperty.Register("ItemHeigt", typeof(double), typeof(CustomScrollBar), new PropertyMetadata((double)0));

  24:  

  25:  

  26:         public static readonly DependencyProperty ScrollOffsetProperty =

  27:             DependencyProperty.Register("ScrollOffset", typeof(double), typeof(CustomScrollBar),

  28:                                         new UIPropertyMetadata(0.0));

  29:  

  30:         public double ScrollOffset

  31:         {

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

  33:             set

  34:             {

  35:                 SetValue(ScrollOffsetProperty, value);

  36:             }

  37:         }

  38:  

  39:         protected override void OnValueChanged(double oldValue, double newValue)

  40:         {

  41:             ScrollOffset = newValue;

  42:             base.OnValueChanged(oldValue, newValue);

  43:         }

  44:  

  45:  

  46:         protected override void OnPreviewMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs e)

  47:         {

  48:             SetThumbPosition(ScrollOffset);

  49:         }

  50:  

  51:         private void SetThumbPosition(double OffsetValue)

  52:         {

  53:             OffsetValue = Math.Round(OffsetValue);

  54:  

  55:             var dif = (OffsetValue % ItemHeigt);

  56:  

  57:             if (dif > ItemHeigt / 2)

  58:             {

  59:                 OffsetValue += ItemHeigt;

  60:             }

  61:             OffsetValue -= dif;

  62:  

  63:             feedBackOffset = true;

  64:             Value = OffsetValue;

  65:         }

  66:     }                                                                                 

ונסביר מה מיוחד בקוד. הוספנו פרופרטי מאוד משמעותי שנקרא ItemHeigt ואותו נעדכן מהxaml בעת השימוש בפקד מהו גובה כל אייטם נגלל (מאחר שהוא dependency property נוכל לעדכן את הערך בזמן ריצה דרך binding). בנוסף יש לי Override ל OnValueChanged ושם אני מעתיק את הערך החדש שהגלילה מספקת, ושולח אותו למשתנה פנימי שהוספנו(dependency property) ונקרא ScrollOfset, נחזור אליו בהמשך.
בנוסף אני עושה Override ל OnPreviewMouseLeftButtonUpוכאן עיקר העבודה שלי אני קורא לפונקציה פנימית שמבצעת חישוב של “עיגול” כלפי מטה או מעלה בהתאם לערך המתחלק באותו פרופרטי שמייצג גובה כל אייטם. כדי שהחבילה הזאת תעבוד יפה נצטרך להתאים שני דברים נוספים: 1. ערך המקסימום של הscrollBar יהיה הגודל החסר בתצוגה שזה (itemHight * items – viewPort) למי שלא מבין מה רשמתי הרגע שיחזור לפוסט הקודם שאני מפנה אליו בתחילת פוסט זה, בהנחה שהScrollBar יודע לבד בהתאם למקסימום כמה לגלול אני בסך הכל מיישר את הגלילה לתחילת השורה הקרובה ביותר. 2. בעת החלת השטח הנגלל נניח בתוך scrollViewer כמו בדוגמא שלי נשאיר את הפרופרטי : CanContentScroll=”false” , עכשיו נמקם את הScrollBar החדש בקוד שלנו נניח כך:
   1: <local:CustomScrollBar Maximum="150" ItemHeigt="50"                     

   2:   ValueChanged="ScrollBar_ValueChanged" Grid.Column="1"/>

ומה שחסר זה מה עושה הפונקציה של valueChanged אז בקוד שלי אני רושם :
   1: private void ScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)

   2:         {

   3:             CustomScrollBarTest.Controls.CustomScrollBar csb = sender as CustomScrollBarTest.Controls.CustomScrollBar;

   4:             Sviewer1.ScrollToVerticalOffset(csb.ScrollOffset);

   5:         }                                                                       

   6:  

ושורת סיכום בנימה אישית, עברתי לLive Writer אחרי מחאות חריפות של קוראים ובלוגרים “מתחרים” אז מעכשיו יהיה נחמד יותר
הוסף תגובה
facebook linkedin twitter email

Leave a Reply

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