我是初学者 WPF。我在工作中开发了一个新项目,我需要插入一个具有多项选择的文件资源管理器控件。
该概念需要类似于 acronis 文件资源管理器:(带有复选框的 Treeview)
看看左边的容器,我需要实现类似的东西,我通过谷歌搜索了很多,我看到了很多实现,但没有什么与这个不相似。
因为我在 WPF 方面没有太多经验,所以我很难开始。
你有一些技巧或类似的项目可以帮助我吗?
我的项目基于 MVVM DP。
谢谢
重塑 Treeview 非常简单,您可以从要绑定的集合开始,即
<Grid>
<TreeView ItemsSource="{Binding Folders}"/>
</Grid>
但是,您随后需要定义如何显示已绑定的数据。我假设您的项目只是 FolderViewModels 和 FileViewModels(都具有 Name 属性)的 IEnumerable(任何列表或数组),所以现在我们需要说明如何显示它们。你可以通过定义一个 DataTemplate 来做到这一点,因为这是一棵树,我们使用一个 HeirarchicalDataTemplate 因为它也定义了 subItems
<Grid.Resources>
<HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}"
ItemsSource="{Binding SubFoldersAndFiles}">
<CheckBox Content="{Binding Name}"/>
</HierarchicalDataTemplate>
<Grid.Resources/>
文件相同但不需要子项
<HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}">
<CheckBox Content="{Binding Name}"/>
</HierarchicalDataTemplate>
所以把它们放在一起你会得到
<Grid>
<Grid.Resources>
<HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}"
ItemsSource="{Binding SubFoldersAndFiles}">
<CheckBox Content="{Binding Name}"/>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type viewModel:FolderViewModel}">
<CheckBox Content="{Binding Name}"/>
</HierarchicalDataTemplate>
<Grid.Resources/>
<TreeView ItemsSource="{Binding Folders}"/>
</Grid>
图标 如果要显示图标,则更改 CheckBox 中的内容,我假设您将在 ViewModel 上定义图像。
<CheckBox>
<CheckBox.Content>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Image}"/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</CheckBox.Content>
选择
最后,您必须处理项目的选择。我建议将 IsSelected 属性添加到您的 FileViewModel 和 FolderViewModels。对于文件来说,这非常简单,它只是一个布尔值。
public class FileViewModel : INotifyProperty
...
public bool IsSelected //Something here to handle get/set and NotifyPropertyChanged that depends on your MVVM framework, I use ReactiveUI a lot so that's this syntax
{
get { return _IsSelected;}
set { this.RaiseAndSetIfChanged(x=>x.IsSelected, value); }
}
和
<CheckBox IsChecked="{Binding IsSelected}">
FolderViewModel 稍微复杂一些,我稍后会看一下逻辑。首先是 Xaml,只需将当前 CheckBox 声明替换为
<CheckBox IsThreeState="True" IsChecked="{Binding IsSelected}">
<!--IsChecked = True, False or null-->
所以现在我们需要返回一组Nullable<bool>
(aka bool?
)。
public bool? IsSelected
{
get
{
if (SubFoldersAndFiles.All(x=>x.IsSelected) return true;
if (SubFoldersAndFiles.All(x=>x.IsSelected==false) return false;
return null;
}
set
{
// We can't set to indeterminate at folder level so we have to set to
// set to oposite of what we have now
if(value == null)
value = !IsSelected;
foreach(var x in SubFoldersAndFiles)
x.IsSelected = value;
}
或者非常相似的东西...
在查看@AlSki 的答案后,我认为它既不直观也不够通用,不适合我的喜好,于是我想出了自己的解决方案。然而,使用我的解决方案的缺点是它需要更多样板。另一方面,它提供了更多的灵活性。
下面的示例假定您使用 .NET 4.6.1 和 C# 6.0。
/// <summary>
/// A base for abstract objects (implements INotifyPropertyChanged).
/// </summary>
[Serializable]
public abstract class AbstractObject : INotifyPropertyChanged
{
/// <summary>
///
/// </summary>
[field: NonSerialized()]
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
///
/// </summary>
/// <param name="propertyName"></param>
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
///
/// </summary>
/// <typeparam name="TKind"></typeparam>
/// <param name="Source"></param>
/// <param name="NewValue"></param>
/// <param name="Names"></param>
protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
{
//Set value if the new value is different from the old
if (!Source.Equals(NewValue))
{
Source = NewValue;
//Notify all applicable properties
Notify?.ForEach(i => OnPropertyChanged(i));
return true;
}
return false;
}
/// <summary>
///
/// </summary>
public AbstractObject()
{
}
}
具有检查状态的对象。
/// <summary>
/// Specifies an object with a checked state.
/// </summary>
public interface ICheckable
{
/// <summary>
///
/// </summary>
bool? IsChecked
{
get; set;
}
}
/// <summary>
///
/// </summary>
public class CheckableObject : AbstractObject, ICheckable
{
/// <summary>
///
/// </summary>
[field: NonSerialized()]
public event EventHandler<EventArgs> Checked;
/// <summary>
///
/// </summary>
[field: NonSerialized()]
public event EventHandler<EventArgs> Unchecked;
/// <summary>
///
/// </summary>
[XmlIgnore]
protected bool? isChecked;
/// <summary>
///
/// </summary>
public virtual bool? IsChecked
{
get
{
return isChecked;
}
set
{
if (SetValue(ref isChecked, value, "IsChecked") && value != null)
{
if (value.Value)
{
OnChecked();
}
else OnUnchecked();
}
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return base.ToString();
//return isChecked.ToString();
}
/// <summary>
///
/// </summary>
protected virtual void OnChecked()
{
Checked?.Invoke(this, new EventArgs());
}
/// <summary>
///
/// </summary>
protected virtual void OnUnchecked()
{
Unchecked?.Invoke(this, new EventArgs());
}
/// <summary>
///
/// </summary>
public CheckableObject() : base()
{
}
/// <summary>
///
/// </summary>
/// <param name="isChecked"></param>
public CheckableObject(bool isChecked = false)
{
IsChecked = isChecked;
}
}
检查系统对象的视图模型:
/// <summary>
///
/// </summary>
public class CheckableSystemObject : CheckableObject
{
#region Properties
/// <summary>
///
/// </summary>
public event EventHandler Collapsed;
/// <summary>
///
/// </summary>
public event EventHandler Expanded;
bool StateChangeHandled = false;
CheckableSystemObject Parent { get; set; } = default(CheckableSystemObject);
ISystemProvider SystemProvider { get; set; } = default(ISystemProvider);
ConcurrentCollection<CheckableSystemObject> children = new ConcurrentCollection<CheckableSystemObject>();
/// <summary>
///
/// </summary>
public ConcurrentCollection<CheckableSystemObject> Children
{
get
{
return children;
}
private set
{
SetValue(ref children, value, "Children");
}
}
bool isExpanded = false;
/// <summary>
///
/// </summary>
public bool IsExpanded
{
get
{
return isExpanded;
}
set
{
if (SetValue(ref isExpanded, value, "IsExpanded"))
{
if (value)
{
OnExpanded();
}
else OnCollapsed();
}
}
}
bool isSelected = false;
/// <summary>
///
/// </summary>
public bool IsSelected
{
get
{
return isSelected;
}
set
{
SetValue(ref isSelected, value, "IsSelected");
}
}
string path = string.Empty;
/// <summary>
///
/// </summary>
public string Path
{
get
{
return path;
}
set
{
SetValue(ref path, value, "Path");
}
}
bool queryOnExpanded = false;
/// <summary>
///
/// </summary>
public bool QueryOnExpanded
{
get
{
return queryOnExpanded;
}
set
{
SetValue(ref queryOnExpanded, value);
}
}
/// <summary>
///
/// </summary>
public override bool? IsChecked
{
get
{
return isChecked;
}
set
{
if (SetValue(ref isChecked, value, "IsChecked") && value != null)
{
if (value.Value)
{
OnChecked();
}
else OnUnchecked();
}
}
}
#endregion
#region CheckableSystemObject
/// <summary>
///
/// </summary>
/// <param name="path"></param>
/// <param name="systemProvider"></param>
/// <param name="isChecked"></param>
public CheckableSystemObject(string path, ISystemProvider systemProvider, bool? isChecked = false) : base()
{
Path = path;
SystemProvider = systemProvider;
IsChecked = isChecked;
}
#endregion
#region Methods
void Determine()
{
//If it has a parent, determine it's state by enumerating all children, but current instance, which is already accounted for.
if (Parent != null)
{
StateChangeHandled = true;
var p = Parent;
while (p != null)
{
p.IsChecked = Determine(p);
p = p.Parent;
}
StateChangeHandled = false;
}
}
bool? Determine(CheckableSystemObject Root)
{
//Whether or not all children and all children's children have the same value
var Uniform = true;
//If uniform, the value
var Result = default(bool?);
var j = false;
foreach (var i in Root.Children)
{
//Get first child's state
if (j == false)
{
Result = i.IsChecked;
j = true;
}
//If the previous child's state is not equal to the current child's state, it is not uniform and we are done!
else if (Result != i.IsChecked)
{
Uniform = false;
break;
}
}
return !Uniform ? null : Result;
}
void Query(ISystemProvider SystemProvider)
{
children.Clear();
if (SystemProvider != null)
{
foreach (var i in SystemProvider.Query(path))
{
children.Add(new CheckableSystemObject(i, SystemProvider, isChecked)
{
Parent = this
});
}
}
}
/// <summary>
///
/// </summary>
protected override void OnChecked()
{
base.OnChecked();
if (!StateChangeHandled)
{
//By checking the root only, all children are checked automatically
foreach (var i in children)
i.IsChecked = true;
Determine();
}
}
/// <summary>
///
/// </summary>
protected override void OnUnchecked()
{
base.OnUnchecked();
if (!StateChangeHandled)
{
//By unchecking the root only, all children are unchecked automatically
foreach (var i in children)
i.IsChecked = false;
Determine();
}
}
/// <summary>
///
/// </summary>
protected virtual void OnCollapsed()
{
Collapsed?.Invoke(this, new EventArgs());
}
/// <summary>
///
/// </summary>
protected virtual void OnExpanded()
{
Expanded?.Invoke(this, new EventArgs());
if (!children.Any<CheckableSystemObject>() || queryOnExpanded)
BeginQuery(SystemProvider);
}
/// <summary>
///
/// </summary>
/// <param name="SystemProvider"></param>
public async void BeginQuery(ISystemProvider SystemProvider)
{
await Task.Run(() => Query(SystemProvider));
}
#endregion
}
用于查询系统对象的实用程序;注意,通过定义自己的SystemProvider
,您可以查询不同类型的系统(即本地或远程)。默认情况下,会查询您的本地系统。如果要显示来自远程服务器(如 FTP)的对象,则需要定义一个SystemProvider
使用适当 Web 协议的对象。
/// <summary>
/// Specifies an object capable of querying system objects.
/// </summary>
public interface ISystemProvider
{
/// <summary>
///
/// </summary>
/// <param name="Path">The path to query.</param>
/// <param name="Source">A source used to make queries.</param>
/// <returns>A list of system object paths.</returns>
IEnumerable<string> Query(string Path, object Source = null);
}
/// <summary>
/// Defines base functionality for an <see cref="ISystemProvider"/>.
/// </summary>
public abstract class SystemProvider : ISystemProvider
{
/// <summary>
///
/// </summary>
/// <param name="Path"></param>
/// <param name="Source"></param>
/// <returns></returns>
public abstract IEnumerable<string> Query(string Path, object Source = null);
}
/// <summary>
/// Defines functionality to query a local system.
/// </summary>
public class LocalSystemProvider : SystemProvider
{
/// <summary>
///
/// </summary>
/// <param name="Path"></param>
/// <param name="Source"></param>
/// <returns></returns>
public override IEnumerable<string> Query(string Path, object Source = null)
{
if (Path.IsNullOrEmpty())
{
foreach (var i in System.IO.DriveInfo.GetDrives())
yield return i.Name;
}
else
{
if (System.IO.Directory.Exists(Path))
{
foreach (var i in System.IO.Directory.EnumerateFileSystemEntries(Path))
yield return i;
}
}
}
}
然后是一个继承的TreeView
,它将所有这些放在一起:
/// <summary>
///
/// </summary>
public class SystemObjectPicker : TreeViewExt
{
#region Properties
/// <summary>
///
/// </summary>
public static DependencyProperty QueryOnExpandedProperty = DependencyProperty.Register("QueryOnExpanded", typeof(bool), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnQueryOnExpandedChanged));
/// <summary>
///
/// </summary>
public bool QueryOnExpanded
{
get
{
return (bool)GetValue(QueryOnExpandedProperty);
}
set
{
SetValue(QueryOnExpandedProperty, value);
}
}
static void OnQueryOnExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.As<SystemObjectPicker>().OnQueryOnExpandedChanged((bool)e.NewValue);
}
/// <summary>
///
/// </summary>
public static DependencyProperty RootProperty = DependencyProperty.Register("Root", typeof(string), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRootChanged));
/// <summary>
///
/// </summary>
public string Root
{
get
{
return (string)GetValue(RootProperty);
}
set
{
SetValue(RootProperty, value);
}
}
static void OnRootChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.As<SystemObjectPicker>().OnRootChanged((string)e.NewValue);
}
/// <summary>
///
/// </summary>
static DependencyProperty SystemObjectsProperty = DependencyProperty.Register("SystemObjects", typeof(ConcurrentCollection<CheckableSystemObject>), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
/// <summary>
///
/// </summary>
ConcurrentCollection<CheckableSystemObject> SystemObjects
{
get
{
return (ConcurrentCollection<CheckableSystemObject>)GetValue(SystemObjectsProperty);
}
set
{
SetValue(SystemObjectsProperty, value);
}
}
/// <summary>
///
/// </summary>
public static DependencyProperty SystemProviderProperty = DependencyProperty.Register("SystemProvider", typeof(ISystemProvider), typeof(SystemObjectPicker), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSystemProviderChanged));
/// <summary>
///
/// </summary>
public ISystemProvider SystemProvider
{
get
{
return (ISystemProvider)GetValue(SystemProviderProperty);
}
set
{
SetValue(SystemProviderProperty, value);
}
}
static void OnSystemProviderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.As<SystemObjectPicker>().OnSystemProviderChanged((ISystemProvider)e.NewValue);
}
#endregion
#region SystemObjectPicker
/// <summary>
///
/// </summary>
public SystemObjectPicker() : base()
{
SetCurrentValue(SystemObjectsProperty, new ConcurrentCollection<CheckableSystemObject>());
SetCurrentValue(SystemProviderProperty, new LocalSystemProvider());
SetBinding(ItemsSourceProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("SystemObjects"),
Source = this
});
}
#endregion
#region Methods
void OnQueryOnExpandedChanged(CheckableSystemObject Item, bool Value)
{
foreach (var i in Item.Children)
{
i.QueryOnExpanded = Value;
OnQueryOnExpandedChanged(i, Value);
}
}
/// <summary>
///
/// </summary>
/// <param name="Value"></param>
protected virtual void OnQueryOnExpandedChanged(bool Value)
{
foreach (var i in SystemObjects)
OnQueryOnExpandedChanged(i, Value);
}
/// <summary>
///
/// </summary>
/// <param name="Provider"></param>
/// <param name="Root"></param>
protected virtual void OnRefreshed(ISystemProvider Provider, string Root)
{
SystemObjects.Clear();
if (Provider != null)
{
foreach (var i in Provider.Query(Root))
{
SystemObjects.Add(new CheckableSystemObject(i, SystemProvider)
{
QueryOnExpanded = QueryOnExpanded
});
}
}
}
/// <summary>
///
/// </summary>
/// <param name="Value"></param>
protected virtual void OnRootChanged(string Value)
{
OnRefreshed(SystemProvider, Value);
}
/// <summary>
///
/// </summary>
/// <param name="Value"></param>
protected virtual void OnSystemProviderChanged(ISystemProvider Value)
{
OnRefreshed(Value, Root);
}
#endregion
}
显然,它比@AlSki 的答案要复杂得多,但是,您再次获得了更大的灵活性,并且已经为您处理了困难的事情。
另外,如果你对这样的事情感兴趣,我已经在我的开源项目(3.1)的最新版本中发布了这段代码;如果没有,上面的示例就是让它工作所需的全部内容。
如果您不下载该项目,请注意以下事项:
IsNullOrEmpty
扩展与 相同string.IsNullOrEmpty()
)。TreeViewExt
是TreeView
我设计的自定义,所以如果您不关心,只需更改TreeViewExt
为TreeView
; 无论哪种方式,您都不必为它定义一个特殊的控制模板,因为它是为与TreeView
的现有设施一起工作而设计的。ObservableCollection
;这样您就可以在后台线程上查询数据而无需费力。将其更改为ObservableCollection
并使所有查询同步或使用您自己的并发ObservableCollection
来保留异步功能。最后,这里是你将如何使用控件:
<Controls.Extended:SystemObjectPicker>
<Controls.Extended:SystemObjectPicker.ItemContainerStyle>
<Style TargetType="TreeViewItem" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Style>
</Controls.Extended:SystemObjectPicker.ItemContainerStyle>
<Controls.Extended:SystemObjectPicker.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<CheckBox
IsChecked="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,5,0"/>
<TextBlock
Text="{Binding Path, Converter={StaticResource FileNameConverter}, Mode=OneWay}"/>
</StackPanel>
</HierarchicalDataTemplate>
</Controls.Extended:SystemObjectPicker.ItemTemplate>
</Controls.Extended:SystemObjectPicker>
去做
CheckableSystemObject
公开系统对象的视图模型;这样,您可以访问与其路径关联的FileInfo
/DirectoryInfo
或其他描述它的数据源。如果对象是远程的,您可能已经定义了自己的类来描述它,如果您有对它的引用,这可能很有用。LocalSystemProvider
也无法解决超过 260 个字符的系统路径;但是,这超出了本项目的范围。版主须知
为方便起见,我引用了自己的开源项目,因为我在最新版本中发布了上述示例;我的目的不是自我推销,所以如果不赞成引用您自己的项目,我将继续删除链接。