c# - WPF,输入受限的Datagrid
问题描述
最近我开始了一个 c# 的新项目——Fireworks 规划软件。我在 WinForms 方面很有经验,但我想学习 WPF。所以我已经通过了几个教程,但还处于起步阶段。
任务是创建一个数据网格,用户在其中放置每个 Firing 点的数据,例如 Cue、EffectTime、DelayBeforeEffect、FiringTime 和 Name(它不完整)。主窗口预览
不,问题是用户必须通过键盘输入时间(EffectTime 等),我想:
- 如果该值类似于 sss.fff(简单数字或十进制数字),程序会自动将其转换为 TimeSpan (h:mm:ss.fff),这实际上是我的射击系统的分辨率。示例:数字 65.528 转换为 0:01:05.528。
- 负值也是允许的,因为在主要烟火表演开始之前有几件事要做
- 我想验证用户输入它是有效的时间值,而不是例如 65.528.20。如果值不正确,我需要取消用户离开编辑的单元格或至少用最后验证的值重写新值
- 值之间存在一些关系,例如:FiringPoint = EffectTime - Delay,或更好
FiringPoint = EffectTime.Subtract(Delay);
正如我习惯的那样,我创建了一个UserControl TimeText
只有一个元素的 -TextBox
我检查并最终正确输入了上面列出的点。DataGrid
然后我DataGridTemplateColumn
在MainWindow中创建了类似的类
FiringPoint
(上面列出的属性),FiringPointManager
带ObservableCollection<FiringPoint>
实现INotifyChange
接口Manager 和 DataGrid 之间的数据绑定。
面对VisualStudio大约20个小时后,确实可以工作了,但是看起来很复杂。因为例如我需要通过 dependencyProperty 等将 DataGridRow.Background 颜色转发到我的 TimeText 控件。
所以最后我的问题是:我的方法是否正确,或者有人可以建议更好的方法来达到相同的功能吗?
感谢您的意见一月
TimeTextBox 的代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace DataGridHratky
{
/// <summary>
/// Interakční logika pro TimeTextBox.xaml
/// </summary>
public partial class TimeTextBox : UserControl, INotifyPropertyChanged
{
#region Overrides
#endregion
#region Enum
#endregion
#region constants
private const string __NEG = "-";
private static readonly string[] __SEP = { ".", "," };
public const string __TFORMAT = @"%h\:mm\:ss\.fff";
private const string __COL = ":";
#endregion
#region private declarations
private bool _isNegative = false;
private double _onlySecondsValue = 0;
private Brush _okBackground;
private Brush _nokBackground = Brushes.OrangeRed;
private Brush _negBackground = Brushes.LavenderBlush;
private TimeSpan _tsValue = new TimeSpan();
private bool _bOnlySeconds = false;
private string _originalValue;
#endregion
#region private methods
private void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Determines if value is only in second format. May be also negative and decimal either with "." or ","
/// </summary>
/// <param name="text">text to check</param>
/// <param name="seconds">Out variable, where seconds should be stored</param>
/// <returns>true if so</returns>
private bool onlySeconds(string text, out double seconds)
{
if (Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator == __SEP[0])
text = text.Replace(__SEP[1], __SEP[0]);
else text = text.Replace(__SEP[0], __SEP[1]);
if (Double.TryParse(text, out seconds))
{
_bOnlySeconds = true;
return true;
} else
{
_bOnlySeconds = false;
return false;
}
}
private bool validate(string time)
{
string _rawValue = time;
if (onlySeconds(_rawValue, out _onlySecondsValue))
{
return true;
}
//check if it is negative
if (_rawValue.StartsWith(__NEG))
{
_isNegative = true;
}
else _isNegative = false;
if (TimeSpan.TryParse(_rawValue, out _tsValue)) return true;
else return false;
}
/// <summary>
/// Determines time based on:
/// - if there is only one : in string means minutes and seconds
/// - if there are two : means hours and minutes and seconds
/// - in both cases milliseconds after . or , may be present
/// </summary>
/// <param name="sTime">String representing validated time</param>
/// <returns>Time span with translated time</returns>
private TimeSpan determineTime(string sTime)
{
int _iColon = Regex.Matches(sTime,__COL).Count;
string _sResult;
TimeSpan _tsDays = new TimeSpan();
if (TimeSpan.TryParse(sTime, out _tsDays))
{
if (_tsDays.Days > 0)
return new TimeSpan(0, _tsDays.Hours, _tsDays.Minutes, _tsDays.Seconds, _tsDays.Milliseconds);
}
TimeSpan _ts = new TimeSpan();
if (_iColon == 1) //minutes and seconds
{
//add 0 hours and 0 days
_sResult = addTimeToTimeSpan(sTime, "0.0:");
}
else if
(_iColon == 2) //hours minutes and seconds
{
//add 0 days
_sResult = addTimeToTimeSpan(sTime, "0.");
}
else _sResult = sTime;
if (TimeSpan.TryParse(_sResult, out _ts))
{
// in all cases remove day
return new TimeSpan(0, _ts.Hours, _ts.Minutes, _ts.Seconds, _ts.Milliseconds);
}
else return _ts;
}
/// <summary>
/// Returns time with added value, moves __NEG sign if present to the from
/// </summary>
/// <param name="sTime">Original time</param>
/// <param name="sTimeToAdd">Time to add</param>
/// <returns>string with complete time</returns>
private string addTimeToTimeSpan(string sTime, string sTimeToAdd)
{
string _sResult;
if (sTime.StartsWith(__NEG))
{
_sResult = __NEG + sTimeToAdd + sTime.Remove(0, 1);
}
else _sResult = sTimeToAdd + sTime;
return _sResult;
}
#endregion
#region Public events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region delegates
#endregion
#region Public delcarations
#region Dependency
public Brush BKColor
{
get { return (Brush)GetValue(BKColorProperty); }
set { SetValue(BKColorProperty, value); }
}
// Using a DependencyProperty as the backing store for BKColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BKColorProperty =
DependencyProperty.Register("BKColor", typeof(Brush), typeof(TimeTextBox), new PropertyMetadata(null));
public bool ReadOnly
{
get { return (bool)GetValue(ReadOnlyProperty); }
set {
SetValue(ReadOnlyProperty, value);
timeTextBox.ReadOnly = value;
}
}
// Using a DependencyProperty as the backing store for ReadOnly. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ReadOnlyProperty =
DependencyProperty.Register("ReadOnly", typeof(bool), typeof(TimeTextBox), new PropertyMetadata(null));
public string TimeText
{
get { return (string)GetValue(TimeTextProperty); }
set {
SetValue(TimeTextProperty, value);
//OnPropertyChanged("TimeText");
}
}
public static readonly DependencyProperty TimeTextProperty =
DependencyProperty.Register("TimeText", typeof(string), typeof(TimeTextBox),
new PropertyMetadata(string.Empty, OnTextPropertyChanged));
private static void OnTextPropertyChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
TimeTextBox myUserControl = dependencyObject as TimeTextBox;
myUserControl.OnPropertyChanged("TimeText");
myUserControl.OnTextPropertyChanged(e);
}
private void OnTextPropertyChanged(DependencyPropertyChangedEventArgs e)
{
txtTime.Text = TimeText;
}
public string Time
{
get { return txtTime.Text; }
set { txtTime.Text = value; }
}
#endregion
/// <summary>
/// True if time span is negative
/// </summary>
public bool IsNegative
{
get
{
return _isNegative;
}
}
/// <summary>
/// Gets or sets validated value background color
/// </summary>
public Brush ValueOKBackground
{
get
{
return _okBackground;
}
set
{
_okBackground = value;
}
}
/// <summary>
/// Gets or sets background color if value is not succesfully validated
/// </summary>
public Brush ValueNotOKBackground
{
get
{
return _okBackground;
}
set
{
_nokBackground = value;
}
}
#endregion
#region Public methods
#endregion
#region Constructors
public TimeTextBox()
{
InitializeComponent();
_okBackground = txtTime.Background;
}
#endregion
private void txtTime_TextChanged(object sender, TextChangedEventArgs e)
{
if (validate(txtTime.Text)) txtTime.Background = BKColor;
else
txtTime.Background = _nokBackground;
}
private void txtTime_LostFocus(object sender, RoutedEventArgs e)
{
//leave if nothing changed
if (txtTime.Text == _originalValue) return;
if (validate(txtTime.Text))
{
if (_bOnlySeconds)
{
int seconds = (int)Math.Truncate(_onlySecondsValue);
int milliseconds = (int)((_onlySecondsValue - seconds) * 1000);
_tsValue = new TimeSpan(0, 0, 0, seconds, milliseconds);
}
else
{
_tsValue = determineTime(txtTime.Text);
}
}
else
{
if (!validate(_originalValue)) //validate methods uses _tsValue to put validated timespan
{
_tsValue = new TimeSpan();
}
}
string _sign = _isNegative ? "-" : "";
txtTime.Text = _sign + _tsValue.ToString(__TFORMAT);
txtTime.Background = _isNegative ? _negBackground : BKColor;
TimeText = txtTime.Text;
txtTime.Background = BKColor;
OnPropertyChanged("UpdateTime");
}
private void txtTime_GotFocus(object sender, RoutedEventArgs e)
{
_originalValue = txtTime.Text;
if (!(validate(txtTime.Text))) txtTime.Text = "";
}
}
}
时间文本 XAML
<UserControl x:Name="timeTextBox" x:Class="DataGridHratky.TimeTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DataGridHratky"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="100">
<!-- DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TimeTextBox}}-->
<Grid x:Name="grdLayout">
<TextBox x:Name="txtTime" TextChanged="txtTime_TextChanged" LostFocus="txtTime_LostFocus" GotFocus="txtTime_GotFocus" BorderThickness="0"
Text="{Binding Path=TimeText, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TimeTextBox}}}"
IsReadOnly="{Binding Path=ReadOnly, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TimeTextBox}}}"
Background="{Binding Path=BKColor, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:TimeTextBox}}}"
VerticalContentAlignment="Center"/>
</Grid>
</UserControl>
主窗口 XAML
<Window x:Class="DataGridHratky.MainWindow"
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:DataGridHratky"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<DataGrid x:Name="dgData" AutoGenerateColumns="False" AddingNewItem="dgData_AddingNewItem" BeginningEdit="dgData_BeginningEdit" CellEditEnding="dgData_CellEditEnding" RowHeight="25" AlternationCount="2" VerticalContentAlignment="Center" >
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<Trigger Property="AlternationIndex" Value="0">
<Setter Property="Background" Value="White" />
</Trigger>
<Trigger Property="AlternationIndex" Value="1">
<Setter Property="Background" Value="WhiteSmoke" />
</Trigger>
<DataTrigger Binding="{Binding Path=Selectable}" Value="False">
<DataTrigger.Setters>
<Setter Property="Background" Value="White" />
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridComboBoxColumn x:Name="colCue" SelectedItemBinding="{Binding SelectedCue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="{Binding AvaiableCues}" Width="50" Header="Cue"/>
<DataGridTextColumn x:Name="colCaption" Width="200" Header="Popis" Binding="{Binding Caption}" />
<DataGridTemplateColumn ClipboardContentBinding="{x:Null}" Header="Čas efektu" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TimeTextBox x:Name="ttbFireEffect" TimeText="{Binding Effect, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
BKColor="{Binding Path=Background, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridComboBoxColumn x:Name="colType" SelectedItemBinding="{Binding SelectedType, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="{Binding AvaiableFWTypes}" Header="Typ" Width="75" />
<DataGridTemplateColumn ClipboardContentBinding="{x:Null}" Header="Prodleni">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TimeTextBox x:Name="ttbDelay" TimeText="{Binding Delay, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ReadOnly = "{Binding IsDelayReadOnly}"
BKColor="{Binding Path=Background, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn ClipboardContentBinding="{x:Null}" Header="Čas odpalu" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:TimeTextBox x:Name="ttbFirePoint" TimeText="{Binding FiringPoint, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
BKColor="{Binding Path=Background, RelativeSource={RelativeSource AncestorType=DataGridRow}}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
FirepointManager 代码
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataGridHratky
{
public class FirepointsManager : INotifyPropertyChanged
{
#region Enum
#endregion
#region constants
#endregion
#region private declarations
private List<string> _avaiableFirepoints = new List<string>();
private List<string> _avaiableFireworks = new List<string>();
#endregion
#region private methods
#endregion
#region Public events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Protected
protected void RaiseChange(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
#endregion
#region delegates
#endregion
#region Public properties
/// <summary>
/// Firepoints Observable collection
/// </summary>
public ObservableCollection<FirePoint> FirePoints { get; set; }
/// <summary>
/// List of avaiable firepoints
/// </summary>
public List<string>AvaiableCues
{
get
{
return _avaiableFirepoints;
}
}
/// <summary>
/// List of pyrotechnics types
/// </summary>
public List<string>AvaiableFWTypes
{
get
{
return _avaiableFireworks;
}
}
#endregion
#region Public methods
/// <summary>
/// Adds a new Firepoint
/// </summary>
/// <param name="f">Firepoint</param>
public void AddFirePoint(FirePoint f)
{
f.SelectedCueChanged += F_SelectedCueChanged;
FirePoints.Add(f);
}
/// <summary>
/// Checks avaible cues and eliminates __MS if already used
/// </summary>
public void CheckSelectedCues()
{
foreach (var FirePoint in FirePoints)
{
if (FirePoint.SelectedCue == FirePoint.__MS)
{
if (_avaiableFirepoints.Contains(FirePoint.__MS))
_avaiableFirepoints.Remove(FirePoint.__MS);
return;
}
}
if (!_avaiableFirepoints.Contains(FirePoint.__MS))
_avaiableFirepoints.Add(FirePoint.__MS);
RaiseChange("CuesAvaiable");
}
private void F_SelectedCueChanged(object sender, EventArgs e)
{
CheckSelectedCues();
}
#endregion
#region Constructors
public FirepointsManager()
{
FirePoints = new ObservableCollection<FirePoint>();
FirePoints.CollectionChanged += FirePoints_CollectionChanged;
_avaiableFirepoints = FirePoint.GeneratedCues();
_avaiableFireworks = FirePoint.FireworksTypes();
}
private void FirePoints_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
CheckSelectedCues();
}
#endregion
}
}
解决方案
推荐阅读
- r - 如何在 R 中设计频率多边形
- android - 不幸的是,flutter android studio 应用程序已停止
- python - Pygame 窗口启动冻结
- c# - 如何使用 MS.Interop 获取 word 文档的标记位置、文本缩进和制表位参数的值
- html - 让 flexbox 垂直增长
- django - 运行 WSGI 应用程序时出错,ModuleNotFoundError: No module named 'mysite'。也找不到静态文件
- clojure - 如何动态定义规格?
- java - 从另一个查询中检索值后如何在查询中插入值
- flutter - 带有 Bloc 的 ImagePicker - Flutter
- asp.net - 如何告诉 services.AddAuthorization 我的自定义用户和角色表在哪里