一、前言
有个项目需要用到时间编辑控件,在大量搜索无果后只能自己自定义一个了。MFC中倒是有这个控件,叫CDateTimeCtrl。大概是这个样子:
二、要实现的功能
- 要实现的功能包含:
- 编辑时、分、秒(可按数字键输入编辑)
- 获取焦点后可实现递增或递减
三、WFP实现原理
四个TextBox和两个TextBlock组和,再加两个按钮应该就能组成这个控件的基本结构了。再设置焦点事件及按键事件可以实现编辑。
Xaml代码如下:
1 <Style TargetType="{x:Type controls:TimeEditer}"> 2 <Setter Property="BorderThickness" Value="1"/> 3 <Setter Property="BorderBrush" Value="#ececec"/> 4 <Setter Property="Hour" Value="00"/> 5 <Setter Property="Minute" Value="00"/> 6 <Setter Property="Second" Value="00"/> 7 <Setter Property="Template"> 8 <Setter.Value> 9 <ControlTemplate TargetType="{x:Type controls:TimeEditer}"> 10 <Border Background="{TemplateBinding Background}" 11 BorderBrush="{TemplateBinding BorderBrush}" 12 BorderThickness="{TemplateBinding BorderThickness}"> 13 <Grid Margin="3 0"> 14 <Grid.ColumnDefinitions> 15 <ColumnDefinition Width="18"/> 16 <ColumnDefinition Width="auto"/> 17 <ColumnDefinition Width="18"/> 18 <ColumnDefinition Width="auto"/> 19 <ColumnDefinition Width="18"/> 20 <ColumnDefinition Width="*"/> 21 </Grid.ColumnDefinitions> 22 <TextBox x:Name="PART_TXTHOUR" HorizontalContentAlignment="Center" Cursor="Arrow" BorderThickness="0" SelectionBrush="White" AutoWordSelection="False" Text="{Binding Hour,RelativeSource={RelativeSource TemplatedParent},StringFormat={}{0:00},UpdateSourceTrigger=PropertyChanged}" Foreground="Black" Focusable="True" IsReadOnly="True" IsReadOnlyCaretVisible="False" VerticalAlignment="Center"/> 23 <TextBlock Text=":" VerticalAlignment="Center" Grid.Column="1"/> 24 <TextBox x:Name="PART_TXTMINUTE" HorizontalContentAlignment="Center" Cursor="Arrow" Grid.Column="2" BorderThickness="0" AutoWordSelection="False" Text="{Binding Minute,RelativeSource={RelativeSource TemplatedParent},StringFormat={}{0:00},UpdateSourceTrigger=PropertyChanged}" Foreground="Black" Focusable="True" IsReadOnly="True" IsReadOnlyCaretVisible="False" VerticalAlignment="Center"/> 25 <TextBlock Text=":" VerticalAlignment="Center" Grid.Column="3"/> 26 <TextBox x:Name="PART_TXTSECOND" HorizontalContentAlignment="Center" Cursor="Arrow" Grid.Column="4" BorderThickness="0" AutoWordSelection="False" Text="{Binding Second,RelativeSource={RelativeSource TemplatedParent},StringFormat={}{0:00},UpdateSourceTrigger=PropertyChanged}" Foreground="Black" Focusable="True" IsReadOnly="True" IsReadOnlyCaretVisible="False" VerticalAlignment="Center"/> 27 <TextBox x:Name="PART_TXT4" Grid.Column="5" Background="Transparent" BorderThickness="0" IsReadOnly="True" AutoWordSelection="False" IsReadOnlyCaretVisible="False" Cursor="Arrow" /> 28 29 <Grid Grid.Column="5" HorizontalAlignment="Right" x:Name="numIncrease" Visibility="{TemplateBinding NumIncreaseVisible}"> 30 <Grid.RowDefinitions> 31 <RowDefinition/> 32 <RowDefinition/> 33 </Grid.RowDefinitions> 34 <controls:ButtonEx x:Name="PART_UP" Focusable="False" ButtonType="Icon" Icon="/BaseControl;component/Images/arrowTop.png" Width="17" Height="11" VerticalAlignment="Bottom"/> 35 <controls:ButtonEx x:Name="PART_DOWN" Focusable="False" ButtonType="Icon" Icon="/BaseControl;component/Images/arrowBottom.png" Width="17" Height="11" VerticalAlignment="Top" Grid.Row="1"/> 36 </Grid> 37 </Grid> 38 </Border> 39 </ControlTemplate> 40 </Setter.Value> 41 </Setter> 42 </Style>
cs代码如下:
1 public class TimeEditer : Control 2 { 3 static TimeEditer() 4 { 5 DefaultStyleKeyProperty.OverrideMetadata(typeof(TimeEditer), new FrameworkPropertyMetadata(typeof(TimeEditer))); 6 } 7 8 private TextBox currentTextBox; 9 private Button btnUp; 10 private Button btnDown; 11 private TextBox txt1; 12 private TextBox txt2; 13 private TextBox txt3; 14 private TextBox txt4; 15 16 public TimeEditer() 17 { 18 var newTime = DateTime.Now.AddMinutes(5); 19 Hour = newTime.Hour; 20 Minute= newTime.Minute; 21 Second= newTime.Second; 22 } 23 24 public override void OnApplyTemplate() 25 { 26 base.OnApplyTemplate(); 27 btnUp =Template.FindName("PART_UP",this) as Button; 28 btnDown = Template.FindName("PART_DOWN", this) as Button; 29 txt1 = Template.FindName("PART_TXTHOUR", this) as TextBox; 30 txt2 = Template.FindName("PART_TXTMINUTE", this) as TextBox; 31 txt3 = Template.FindName("PART_TXTSECOND", this) as TextBox; 32 txt4 = Template.FindName("PART_TXT4", this) as TextBox; 33 34 35 txt1.GotFocus += TextBox_GotFocus; 36 txt2.GotFocus += TextBox_GotFocus; 37 txt3.GotFocus += TextBox_GotFocus; 38 txt1.LostFocus += TextBox_LostFocus; 39 txt2.LostFocus += TextBox_LostFocus; 40 txt3.LostFocus += TextBox_LostFocus; 41 txt1.KeyDown += Txt1_KeyDown; 42 txt2.KeyDown += Txt1_KeyDown; 43 txt3.KeyDown += Txt1_KeyDown; 44 45 txt4.GotFocus += TextBox2_GotFocus; 46 txt4.LostFocus += TextBox2_LostFocus; 47 48 this.GotFocus += UserControl_GotFocus; 49 this.LostFocus += UserControl_LostFocus; 50 51 this.Repeater(btnUp, (t, num, reset) => 52 { 53 if (reset && t.Interval == 500) 54 t.Interval = 50; 55 Dispatcher.Invoke(new Action(() => 56 { 57 if (currentTextBox.Name == "PART_TXTHOUR") 58 { 59 int.TryParse(currentTextBox.Text, out int numResult); 60 numResult += num; 61 if (numResult >= 24) 62 numResult = 0; 63 if (numResult < 0) 64 numResult = 23; 65 currentTextBox.Text = numResult.ToString("00"); 66 } 67 else if (currentTextBox.Name == "PART_TXTMINUTE") 68 { 69 int.TryParse(currentTextBox.Text, out int numResult); 70 numResult += num; 71 if (numResult >= 60) 72 numResult = 0; 73 if (numResult < 0) 74 numResult = 59; 75 currentTextBox.Text = numResult.ToString("00"); 76 } 77 else if (currentTextBox.Name == "PART_TXTSECOND") 78 { 79 int.TryParse(currentTextBox.Text, out int numResult); 80 numResult += num; 81 if (numResult >= 60) 82 numResult = 0; 83 if (numResult < 0) 84 numResult = 59; 85 currentTextBox.Text = numResult.ToString("00"); 86 } 87 })); 88 89 }, 1); 90 this.Repeater(btnDown, (t, num, reset) => 91 { 92 if (reset && t.Interval == 500) 93 t.Interval = 50; 94 Dispatcher.Invoke(new Action(() => 95 { 96 if (currentTextBox.Name == "PART_TXTHOUR") 97 { 98 int.TryParse(currentTextBox.Text, out int numResult); 99 numResult += num; 100 if (numResult >= 24) 101 numResult = 0; 102 if (numResult < 0) 103 numResult = 23; 104 currentTextBox.Text = numResult.ToString("00"); 105 } 106 else if (currentTextBox.Name == "PART_TXTMINUTE") 107 { 108 int.TryParse(currentTextBox.Text, out int numResult); 109 numResult += num; 110 if (numResult >= 60) 111 numResult = 0; 112 if (numResult < 0) 113 numResult = 59; 114 currentTextBox.Text = numResult.ToString("00"); 115 } 116 else if (currentTextBox.Name == "PART_TXTSECOND") 117 { 118 int.TryParse(currentTextBox.Text, out int numResult); 119 numResult += num; 120 if (numResult >= 60) 121 numResult = 0; 122 if (numResult < 0) 123 numResult = 59; 124 currentTextBox.Text = numResult.ToString("00"); 125 } 126 })); 127 128 }, -1); 129 } 130 private void TextBox_GotFocus(object sender, RoutedEventArgs e) 131 { 132 var textBox = sender as TextBox; 133 textBox.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#0078d7")); 134 textBox.Foreground = new SolidColorBrush(Colors.White); 135 currentTextBox = textBox; 136 137 } 138 139 private void TextBox_LostFocus(object sender, RoutedEventArgs e) 140 { 141 var textBox = sender as TextBox; 142 textBox.Background = new SolidColorBrush(Colors.Transparent); 143 textBox.Foreground = new SolidColorBrush(Colors.Black); 144 int.TryParse(currentTextBox.Text, out int numResult); 145 currentTextBox.Text = numResult.ToString("00"); 146 } 147 148 private void UserControl_LostFocus(object sender, RoutedEventArgs e) 149 { 150 this.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 234, 234, 234)); 151 NumIncreaseVisible = Visibility.Collapsed; 152 } 153 154 private void UserControl_GotFocus(object sender, RoutedEventArgs e) 155 { 156 this.BorderBrush = new SolidColorBrush(Color.FromArgb(255, 0, 120, 215)); 157 NumIncreaseVisible = Visibility.Visible; 158 } 159 160 private void TextBox2_GotFocus(object sender, RoutedEventArgs e) 161 { 162 txt3.Focus(); 163 } 164 165 private void TextBox2_LostFocus(object sender, RoutedEventArgs e) 166 { 167 168 } 169 170 public void Repeater(Button btn, Action<Timer, int, bool> callBack, int num, int interval = 500) 171 { 172 var timer = new Timer { Interval = interval }; 173 timer.Elapsed += (sender, e) => 174 { 175 callBack?.Invoke(timer, num, true); 176 }; 177 btn.PreviewMouseLeftButtonDown += (sender, e) => 178 { 179 callBack?.Invoke(timer, num, false); 180 timer.Start(); 181 }; 182 btn.PreviewMouseLeftButtonUp += (sender, e) => 183 { 184 timer.Interval = 500; 185 timer.Stop(); 186 }; 187 } 188 189 private void Txt1_KeyDown(object sender, KeyEventArgs e) 190 { 191 int.TryParse(currentTextBox.Text, out int numResult); 192 193 if ((int)e.Key >= 34 && (int)e.Key <= 43) 194 { 195 196 if (currentTextBox.Text.Length == 1) 197 { 198 int.TryParse(currentTextBox.Text + ((int)e.Key - 34).ToString(), out int preNumResult); 199 if (currentTextBox.Name == "PART_TXTHOUR") 200 { 201 if (preNumResult >= 24) 202 { 203 return; 204 } 205 } 206 else if (currentTextBox.Name == "PART_TXTMINUTE") 207 { 208 if (preNumResult >= 60) 209 { 210 return; 211 } 212 } 213 else if (currentTextBox.Name == "PART_TXTSECOND") 214 { 215 if (preNumResult >= 60) 216 { 217 return; 218 } 219 } 220 221 currentTextBox.Text += ((int)e.Key - 34).ToString(); 222 } 223 else 224 { 225 currentTextBox.Text = ((int)e.Key - 34).ToString(); 226 } 227 } 228 229 if ((int)e.Key >= 74 && (int)e.Key <= 83) 230 { 231 232 if (currentTextBox.Text.Length == 1) 233 { 234 int.TryParse(currentTextBox.Text + ((int)e.Key - 74).ToString(), out int preNumResult); 235 if (currentTextBox.Name == "PART_TXTHOUR") 236 { 237 if (preNumResult >= 24) 238 { 239 return; 240 } 241 } 242 else if (currentTextBox.Name == "PART_TXTMINUTE") 243 { 244 if (preNumResult >= 60) 245 { 246 return; 247 } 248 } 249 else if (currentTextBox.Name == "PART_TXTSECOND") 250 { 251 if (preNumResult >= 60) 252 { 253 return; 254 } 255 } 256 currentTextBox.Text += ((int)e.Key - 74).ToString(); 257 } 258 else 259 { 260 currentTextBox.Text = ((int)e.Key - 74).ToString(); 261 } 262 } 263 264 } 265 266 267 268 public int Hour 269 { 270 get { return (int)GetValue(HourProperty); } 271 set { SetValue(HourProperty, value); } 272 } 273 274 // Using a DependencyProperty as the backing store for Hour. This enables animation, styling, binding, etc... 275 public static readonly DependencyProperty HourProperty = 276 DependencyProperty.Register("Hour", typeof(int), typeof(TimeEditer), new PropertyMetadata(DateTime.Now.Hour)); 277 278 279 public int Minute 280 { 281 get { return (int)GetValue(MinuteProperty); } 282 set { SetValue(MinuteProperty, value); } 283 } 284 285 // Using a DependencyProperty as the backing store for Minute. This enables animation, styling, binding, etc... 286 public static readonly DependencyProperty MinuteProperty = 287 DependencyProperty.Register("Minute", typeof(int), typeof(TimeEditer), new PropertyMetadata(DateTime.Now.Minute)); 288 289 290 public int Second 291 { 292 get { return (int)GetValue(SecondProperty); } 293 set { SetValue(SecondProperty, value); } 294 } 295 296 // Using a DependencyProperty as the backing store for Second. This enables animation, styling, binding, etc... 297 public static readonly DependencyProperty SecondProperty = 298 DependencyProperty.Register("Second", typeof(int), typeof(TimeEditer), new PropertyMetadata(DateTime.Now.Second)); 299 300 301 302 public Visibility NumIncreaseVisible 303 { 304 get { return (Visibility)GetValue(NumIncreaseVisibleProperty); } 305 set { SetValue(NumIncreaseVisibleProperty, value); } 306 } 307 308 // Using a DependencyProperty as the backing store for NumIncreaseVisible. This enables animation, styling, binding, etc... 309 public static readonly DependencyProperty NumIncreaseVisibleProperty = 310 DependencyProperty.Register("NumIncreaseVisible", typeof(Visibility), typeof(TimeEditer), new PropertyMetadata(Visibility.Collapsed)); 311 312 313 }
四、实现效果(可双向绑定)
五、小结
实现的过程还是比较曲折的,需要了解TextBox相关属性,刚开始不清楚走了很多弯路,相关属性可以在这里查看https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.controls.textbox?view=netframework-4.8 。另外一个就是实现数字递增递减的方案,刚开始始终没法实现快速递增和递减,只能开线程匀速增减,还很慢,太快的话又会影响单次点击的效果。最后是使用Timmer控件,通过修改Interval实现了,哈哈。
源码放git了: