目录

介绍

TreeView的背景

ViewModel的背景

究竟是什么让TreeView如此困难?

ViewModel来救援

演示解决方案

演示 1 – 带有文本搜索的家谱

PersonViewModel

用户界面

FamilyTreeViewModel

演示 2 – 按需加载的地理细分

结论


介绍

本文探讨如何使用ViewModel模式来更轻松地使用WPF中的TreeView控件。在此过程中,我们研究了为什么人们在使用WPF TreeView时经常遇到困难、什么是ViewModel,以及展示如何将TreeView与ViewModel结合的两个演示应用程序。其中一个演示展示了如何创建可搜索的TreeView,另一个演示如何实现延迟加载(也称为按需加载)。

TreeView的背景

WPF中的TreeView控件获得了不应有的坏名声。许多人尝试使用它,发现它非常困难。问题是人们经常尝试以与针对Windows窗体控件TreeView编写代码相同的方式使用它。为了利用WPF TreeView的广泛功能,您不能使用与Windows窗体中相同的编程技术。这是WPF如何要求您转换思维方式以适当地利用平台的另一个例子。

在Window窗体中,使用TreeView控件非常容易,因为它非常简单。这种简单性源于以下事实:Windows窗体TreeView完全不灵活,不支持UI虚拟化,对可视化自定义提供零可能性,并且由于它不支持数据绑定,因此需要您将数据存储在其节点中。WinForms TreeView “对于政府工作来说已经足够好了”。

相比之下,WPF TreeView极其灵活,固有地支持UI虚拟化(即,TreeViewItem是按需创建的),允许完全可视化定制,并且完全支持数据绑定。这些出色的功能是有代价的。它们使TreeView控件比WinForms更复杂。一旦您学会了如何正确使用 WPF TreeView,这些复杂性就消失了,并且很容易利用控件的全部功能。

ViewModel的背景

早在2005年,John Gossman在博客中谈到了他在Microsoft的团队用来创建Expression Blend(当时称为“Sparkle”)的模型-视图-视图模型模式。它与Martin Fowler的Presentation Model模式非常相似,只是它利用WPF丰富的数据绑定填补了表示模型​​和视图之间的空白。在Dan Crevier撰写了他精彩的DataModel-View-ViewModel系列博文之后,(D)MVVM模式开始流行起来。

(Data)Model-View-ViewModel模式与经典的Model-View-Presenter相似,只是您有一个为View量身定制的模型,称为ViewModel。ViewModel包含所有必要的特定于UI的界面和属性,以便于开发用户界面。View绑定到ViewModel,并执行命令以向其请求操作。ViewModel反过来与Model通信,并告诉它更新以响应用户交互。

这使得为​​应用程序创建用户界面(UI)变得更加容易。在应用程序上添加UI越容易,对于技术有挑战的视觉设计师来说,在Blend中创建漂亮的UI就越容易。此外,UI与应用程序功能的耦合越松散,该功能就越可测试。谁不想要一个漂亮的UI和一套干净、有效的单元测试?

究竟是什么让TreeView如此困难?

只要您以正确的方式使用它,它实际上很容易使用TreeView。矛盾的是,以正确的方式使用它意味着根本不直接使用它!当然,你需要设置属性并直接在TreeView中调用临时方法。这是不可避免的,这样做并没有错。但是,如果您发现自己深入控件的核心,那么您可能没有采取最好的方法。如果你的TreeView是数据绑定的,并且您发现自己试图以编程方式上下移动项,那么您做事的方式不正确。如果您发现自己正在挂钩ItemContainerGenerator的StatusChanged事件,以便您可以访问最终被创建时的TreeViewItem子项,你就偏离了轨道!相信我; 它不必如此丑陋和困难。有一个更好的方法!

对待WPF TreeView和对待WinForms TreeView的基本问题是,正如我前面提到的,它们是非常不同的控件。WPF TreeView允许您通过数据绑定生成其项。这意味着它将为您创建TreeViewItem。由于TreeViewItem是由控件而不是由您生成的,因此不能保证数据对象的对应项TreeViewItem在您需要时存在。您必须询问TreeView的ItemContainerGenerator是否已经为您生成了TreeViewItem。如果没有,您必须挂钩它的StatusChanged事件,以便在它创建其子元素时得到通知。

乐趣不止于此!如果您想获得嵌套在树中深处的一个TreeViewItem,您必须询问该项的父级/自己的TreeViewItem,而不是TreeView控件,如果ItemContainerGenerator创建了该项。但是,如果其父级TreeViewItem尚未创建它,您如何获得对其的引用?如果还没有生成父级的父级怎么办?依此类推,依此类推,依此类推。这可能是相当折磨人的。

如您所见,WPF TreeView是一个复杂的野兽。如果您尝试以错误的方式使用它,那将并不容易。幸运的是,如果你以正确的方式使用它,它就是小菜一碟。那么,让我们看看如何正确使用它……

ViewModel来救援

WPF很棒,因为它实际上要求您将应用程序的数据与UI分开。上一节中列出的所有问题都源于试图违背常规并将UI视为后备存储。一旦您不再将其TreeView视为放置数据的地方,而是开始将其视为展示数据的地方,一切都会开始顺利进行。这就是ViewModel的想法发挥作用的地方。

与其编写在TreeView中的项目上下移动的代码,不如创建一个TreeView绑定到的ViewModel,然后编写操作您的ViewModel的代码。这不仅可以让您忽略TreeView的复杂性,还可以让您编写可以轻松进行单元测试的代码。为与TreeView的运行时行为密切相关的类编写有意义的单元测试几乎是不可能的,但是为对这种废话一无所知的类编写单元测试很容易。

现在,是时候看看如何实现这些概念了。

演示解决方案

本文附带两个演示应用程序,可在本页顶部下载。该解决方案有两个项目。BusinessLib类库项目包含简单的领域类,仅用作数据传输对象。它还包含一个实例化并返回这些数据传输对象的Database类。另一个项目TreeViewWithViewModelDemo包含示例应用程序。这些应用程序使用BusinessLib程序集返回的对象,并将它们包装在ViewModel中,然后再将它们显示在TreeView中。

这是解决方案的解决方案资源管理器的屏幕截图:

演示 1 – 带有文本搜索的家谱

我们将检查的第一个演示应用程序使用家谱填充TreeView。它提供了一种搜索功能,在UI底部可供用户使用。该演示可以在下面的屏幕截图中看到:

当用户输入一些搜索文本并按Enter或单击“查找”按钮时,将显示第一个匹配项。继续搜索将循环通过每个匹配的项目。所有这些逻辑都在ViewModel中。在深入了解ViewModel的工作原理之前,让我们先检查一下周围的代码。这是TextSearchDemoControl的代码隐藏:

public partial class TextSearchDemoControl : UserControl
{readonly FamilyTreeViewModel _familyTree;  public TextSearchDemoControl(){InitializeComponent();// Get raw family tree data from a database.Person rootPerson = Database.GetFamilyTree();// Create UI-friendly wrappers around the // raw data objects (i.e. the view-model)._familyTree = new FamilyTreeViewModel(rootPerson);// Let the UI bind to the view-model.base.DataContext = _familyTree;}void searchTextBox_KeyDown(object sender, KeyEventArgs e){if (e.Key == Key.Enter)_familyTree.SearchCommand.Execute(null);}
}

构造函数展示了我们如何将原始数据对象转换为ViewModel,然后设置为UserControl的DataContext。该类Person位于BusinessLib程序集中,非常简单:

/// <summary>
/// A simple data transfer object (DTO) that contains raw data about a person.
/// </summary>
public class Person
{readonly List<Person> _children = new List<Person>();public IList<Person> Children{get { return _children; }}public string Name { get; set; }
}

PersonViewModel

由于Person类是应用程序的数据访问层返回的内容,因此它绝对不适合UI使用。每个Person对象最终都会被PersonViewModel类的一个实例包装,使其具有扩展的语义,例如被扩展和选择。如上所示,该类FamilyTreeViewModel启动了将Person对象包装在PersonViewModel对象内部的过程,如该类的构造函数所示:

public FamilyTreeViewModel(Person rootPerson)
{_rootPerson = new PersonViewModel(rootPerson);_firstGeneration = new ReadOnlyCollection<PersonViewModel>(new PersonViewModel[] { _rootPerson });_searchCommand = new SearchFamilyTreeCommand(this);
}

私有PersonViewModel构造函数递归地遍历族谱,将每个Person对象包装在一个PersonViewModel.中,这些构造函数如下所示:

public PersonViewModel(Person person): this(person, null)
{
}private PersonViewModel(Person person, PersonViewModel parent)
{_person = person;_parent = parent;_children = new ReadOnlyCollection<PersonViewModel>((from child in _person.Childrenselect new PersonViewModel(child, this)).ToList<PersonViewModel>());
}

PersonViewModel有两种成员:与表示相关的成员和与Person的状态相关的成员。表示属性是TreeViewItem将绑定的内容,状态属性由TreeViewItem的内容绑定。表示属性之一,IsSelected,如下所示:

/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public bool IsSelected
{get { return _isSelected; }set{if (value != _isSelected){_isSelected = value;this.OnPropertyChanged("IsSelected");}}
}

此属性与“person”无关,而只是用于将View与ViewModel同步的状态。请注意,该属性的setter调用了一个OnPropertyChanged方法,该方法最终引发了对象的PropertyChanged事件。该事件是INotifyPropertyChanged接口的唯一成员。INotifyPropertyChanged是一个特定于UI的接口,这就是PersonViewModel类实现它的原因,而不是Person类。

PersonViewModel上的演示成员的一个更有趣的例子是IsExpanded属性。这个属性很容易解决确保数据对象的对应TreeViewItem在必要时被扩展的问题。请记住,当直接针对TreeView自身进行编程时,这些类型的问题可能非常棘手且难以处理。

/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{get { return _isExpanded; }set{if (value != _isExpanded){_isExpanded = value;this.OnPropertyChanged("IsExpanded");}// Expand all the way up to the root.if (_isExpanded && _parent != null)_parent.IsExpanded = true;}
}

正如我之前提到的,PersonViewModel还具有与其底层Person对象的状态相关的属性。这是一个例子:

public string Name
{get { return _person.Name; }
}

用户界面

绑定到PersonViewModel树的TreeView的XAML非常简单。请注意,TreeViewItem和PersonViewModel对象之间的连接在于控件的ItemContainerStyle中:

<TreeView ItemsSource="{Binding FirstGeneration}"><TreeView.ItemContainerStyle><!-- This Style binds a TreeViewItem to a PersonViewModel. --><Style TargetType="{x:Type TreeViewItem}"><Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /><Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" /><Setter Property="FontWeight" Value="Normal" /><Style.Triggers><Trigger Property="IsSelected" Value="True"><Setter Property="FontWeight" Value="Bold" /></Trigger></Style.Triggers></Style></TreeView.ItemContainerStyle><TreeView.ItemTemplate><HierarchicalDataTemplate ItemsSource="{Binding Children}"><TextBlock Text="{Binding Name}" /></HierarchicalDataTemplate></TreeView.ItemTemplate>
</TreeView>

该演示的UI的另一部分是搜索区域。该区域为用户提供了一个TextBox输入搜索字符串的区域,以及一个“查找”按钮以执行对家谱的搜索。这是搜索区域的XAML:

<StackPanel HorizontalAlignment="Center" Margin="4" Orientation="Horizontal"><TextBlock Text="Search for:" /><TextBox x:Name="searchTextBox"KeyDown="searchTextBox_KeyDown" Margin="6,0"Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"Width="150"/><Button Command="{Binding SearchCommand}" Content="_Find" Padding="8,0" />
</StackPanel>

现在,让我们看看支持这个用户界面的FamilyTreeViewModel中的代码。

FamilyTreeViewModel

搜索功能封装在FamilyTreeViewModel类中。TextBox包含的搜索文本绑定到SearchText属性,声明如下:

/// <summary>
/// Gets/sets a fragment of the name to search for.
/// </summary>
public string SearchText
{get { return _searchText; }set{if (value == _searchText)return;_searchText = value;_matchingPeopleEnumerator = null;}
}

当用户单击“查找”按钮时,将执行FamilyTreeViewModel的SearchCommand命令。该命令类嵌套在FamilyTreeViewModel中,但向视图公开它的属性是public。该代码如下所示:

/// <summary>
/// Returns the command used to execute a search in the family tree.
/// </summary>
public ICommand SearchCommand
{get { return _searchCommand; }
}private class SearchFamilyTreeCommand : ICommand
{readonly FamilyTreeViewModel _familyTree;public SearchFamilyTreeCommand(FamilyTreeViewModel familyTree){_familyTree = familyTree;}public bool CanExecute(object parameter){return true;}event EventHandler ICommand.CanExecuteChanged{// I intentionally left these empty because// this command never raises the event, and// not using the WeakEvent pattern here can// cause memory leaks. WeakEvent pattern is// not simple to implement, so why bother.add { }remove { }}public void Execute(object parameter){_familyTree.PerformSearch();}
}

如果您熟悉我的WPF技术和理念,您可能会惊讶地发现我在这里没有使用路由命令。出于多种原因,我通常更喜欢路由命令,但在这种情况下,使用简单的ICommand实现会更干净、更简单。注意,一定要阅读CanExecuteChanged事件声明中的注释。

搜索逻辑完全不依赖于TreeView或TreeViewItem。它只是遍历ViewModel对象并设置ViewModel属性。尝试直接针对TreeView API编写此代码将更加困难且容易出错。这是我的搜索逻辑:

IEnumerator<PersonViewModel> _matchingPeopleEnumerator;
string _searchText = String.Empty;void PerformSearch()
{if (_matchingPeopleEnumerator == null || !_matchingPeopleEnumerator.MoveNext())this.VerifyMatchingPeopleEnumerator();var person = _matchingPeopleEnumerator.Current;if (person == null)return;// Ensure that this person is in view.if (person.Parent != null)person.Parent.IsExpanded = true;person.IsSelected = true;
}void VerifyMatchingPeopleEnumerator()
{var matches = this.FindMatches(_searchText, _rootPerson);_matchingPeopleEnumerator = matches.GetEnumerator();if (!_matchingPeopleEnumerator.MoveNext()){MessageBox.Show("No matching names were found.","Try Again",MessageBoxButton.OK,MessageBoxImage.Information);}
}IEnumerable<PersonViewModel> FindMatches(string searchText, PersonViewModel person)
{if (person.NameContainsText(searchText))yield return person;foreach (PersonViewModel child in person.Children)foreach (PersonViewModel match in this.FindMatches(searchText, child))yield return match;
}

演示 2 – 按需加载的地理细分

下一个演示应用程序使用有关一个国家/地区的各个地方的信息填充TreeView。它处理三种不同类型的对象:Region、State和City。这些类型中的每一个都有一个对应的表示类,TreeViewItem绑定到该类。

每个表示类都派生自TreeViewItemViewModel基类,该基类提供了在之前的演示PersonViewModel类中看到的所有特定于表示的功能。此外,此演示中的项目是延迟加载的,这意味着程序不会获取项目的子项并将它们添加到对象图中,直到用户尝试查看它们。您可以在下面的屏幕截图中看到此演示:

正如我上面提到的,这里有三个独立的数据类,每个数据类都有一个关联的表示类。所有这些表示类都派生自TreeViewItemViewModel,由该接口描述:

interface ITreeViewItemViewModel : INotifyPropertyChanged
{ObservableCollection<TreeViewItemViewModel> Children { get; }bool HasDummyChild { get; }bool IsExpanded { get; set; }bool IsSelected { get; set; }TreeViewItemViewModel Parent { get; }
}

LoadOnDemandDemoControl的代码隐藏如下所示:

public partial class LoadOnDemandDemoControl : UserControl
{public LoadOnDemandDemoControl(){InitializeComponent();Region[] regions = Database.GetRegions();CountryViewModel viewModel = new CountryViewModel(regions);base.DataContext = viewModel;}
}

该构造函数只是从BusinessLib程序集中加载一些数据对象,从中创建一些UI友好的包装器,然后让视图绑定到这些包装器。视图DataContext设置为此类的一个实例:

/// <summary>
/// The ViewModel for the LoadOnDemand demo. This simply
/// exposes a read-only collection of regions.
/// </summary>
public class CountryViewModel
{readonly ReadOnlyCollection<RegionViewModel> _regions;public CountryViewModel(Region[] regions){_regions = new ReadOnlyCollection<RegionViewModel>((from region in regionsselect new RegionViewModel(region)).ToList());}public ReadOnlyCollection<RegionViewModel> Regions{get { return _regions; }}
}

有趣的代码在TreeViewItemViewModel中。 它主要是之前演示的PersonViewModel中的演示逻辑的副本,但有一个有趣的转折。TreeViewItemViewModel具有对子项按需加载的内置支持。该逻辑存在于类的构造函数和IsExpanded属性的设置器中。按需加载TreeViewItemViewModel逻辑如下图所示:

protected TreeViewItemViewModel(TreeViewItemViewModel parent, bool lazyLoadChildren)
{_parent = parent;_children = new ObservableCollection<TreeViewItemViewModel>();if (lazyLoadChildren)_children.Add(DummyChild);
}/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{get { return _isExpanded; }set{if (value != _isExpanded){_isExpanded = value;this.OnPropertyChanged("IsExpanded");}// Expand all the way up to the root.if (_isExpanded && _parent != null)_parent.IsExpanded = true;// Lazy load the child items, if necessary.if (this.HasDummyChild){this.Children.Remove(DummyChild);this.LoadChildren();}}
}/// <summary>
/// Returns true if this object's Children have not yet been populated.
/// </summary>
public bool HasDummyChild
{get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
}/// <summary>
/// Invoked when the child items need to be loaded on demand.
/// Subclasses can override this to populate the Children collection.
/// </summary>
protected virtual void LoadChildren()
{
}

加载对象的子项的实际工作留给子类处理。它们重写该LoadChildren方法以提供加载子项的特定于类型的实现。RegionViewModel如下所示,该类覆盖此方法以加载State对象并创建StateViewModel包装对象。

public class RegionViewModel : TreeViewItemViewModel
{readonly Region _region;public RegionViewModel(Region region) : base(null, true){_region = region;}public string RegionName{get { return _region.RegionName; }}protected override void LoadChildren(){foreach (State state in Database.GetStates(_region))base.Children.Add(new StateViewModel(state, this));}
}

此演示的用户界面仅包含一个TreeView,它使用以下XAML进行配置:

<TreeView ItemsSource="{Binding Regions}"><TreeView.ItemContainerStyle><!-- This Style binds a TreeViewItem to a TreeViewItemViewModel. --><Style TargetType="{x:Type TreeViewItem}"><Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /><Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" /><Setter Property="FontWeight" Value="Normal" /><Style.Triggers><Trigger Property="IsSelected" Value="True"><Setter Property="FontWeight" Value="Bold" /></Trigger></Style.Triggers></Style></TreeView.ItemContainerStyle><TreeView.Resources><HierarchicalDataTemplate DataType="{x:Type local:RegionViewModel}" ItemsSource="{Binding Children}"><StackPanel Orientation="Horizontal"><Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" /><TextBlock Text="{Binding RegionName}" /></StackPanel></HierarchicalDataTemplate><HierarchicalDataTemplate DataType="{x:Type local:StateViewModel}" ItemsSource="{Binding Children}"><StackPanel Orientation="Horizontal"><Image Width="16" Height="16" Margin="3,0" Source="Images\State.png" /><TextBlock Text="{Binding StateName}" /></StackPanel></HierarchicalDataTemplate><DataTemplate DataType="{x:Type local:CityViewModel}"><StackPanel Orientation="Horizontal"><Image Width="16" Height="16" Margin="3,0" Source="Images\City.png" /><TextBlock Text="{Binding CityName}" /></StackPanel></DataTemplate></TreeView.Resources>
</TreeView>

结论

如果您曾经使用过WPF TreeView,也许这篇文章已经阐明了使用该控件的另一种方法。一旦你开始顺其自然,不再试图逆流而上,WPF会让你的生活变得非常轻松。困难的部分是放弃你来之不易的知识和技能,并采用完全不同的方式来解决相同的问题。

https://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode

使用ViewModel模式简化WPF TreeView相关推荐

  1. 在WPF TreeView中使用复选框

    目录 介绍 背景 细节决定成败 功能要求 将Smarts放入ViewModel 树视图配置 将TreeViewItem变成ToggleButton Aero主题中的复选框错误 介绍 本文回顾了一个WP ...

  2. WPF TreeView ItemContainerStyle和ItemTemplate ViewModel Binding IsSelected

    WPF TreeView ItemContainerStyle和ItemTemplate ViewModel Binding IsSelected 记录一下 直接上代码 记录一下 直接上代码 < ...

  3. WPF TreeView HierarchicalDataTemplate

    原文 WPF TreeView HierarchicalDataTemplate   <StackPanel Margin="0,0,0,0"><StackPan ...

  4. WPF TreeView 绑定(demo 转)

    WPF TreeView 绑定 2018年01月23日 13:55:32 余生余我 阅读数:563 前台: <TreeView x:Name="tree" ItemsSour ...

  5. WPF TreeView IsExpanded 绑定不上的问题

    最近项目上需要通过MVVM来控制TreeView,其中需要需要控制通过搜索来定位某个节点,正常逻辑下,首先通过需要在树上面找到该节点,然后选中该节点,并将该节点的父节点展开,这个时候需要通过MVVM来 ...

  6. wpf展开树节点_【转】WPF TreeView如何展开到某个节点

    初用WPF的TreeView控件,需要将树展开到某个特定的TreeViewItem,各种方法都尝试过,却发现代码总在某些情况下出错,然后仔细研究,才发现其中的曲折. 解决问题的思路是,得到从树的根节点 ...

  7. WPF Treeview第三层横向排列

    WPF 第三级横向排列效果,左侧使用WrapPanel,右侧使用StackPanel,效果见下图: 代码如下: Mainwindow的xaml如下: <Window x:Class=" ...

  8. WPF TreeView 相关技巧

    WPF 中的 TreeView 相关实用技巧如下 响应 TreeViewItem 的双击事件 这是一个较怪异的问题,如果在 TreeView 的 MouseDoubleClick 中去处理 Selec ...

  9. WPF TreeView的使用

    WPF提供了treeView控件,利用该控件开发者可以将数据分层显示在树结构中.当然其中需要用到Binding的机制,有用的类包括:ObjectDataProvider.DataTemplate.Hi ...

最新文章

  1. “tensorFromBlob”: 不是“at::DeprecatedTypeProperties”的成员
  2. retain/copy/assign区别
  3. swift5自定义构造函数和自定义变量报required init?(coder: NSCoder) { fatalError(“init(coder:) has not bee错问题
  4. 美国0封伊朗已经6天了,伊石油出口真归零了吗?
  5. [转载]IIS7报500.23错误的解决方法
  6. 逻辑回归实现多分类任务(python+TensorFlow+mnist)
  7. 平安dms开发java_Spring DMS模板同步接收非持久用户消息丢失
  8. 我的webpack路
  9. 离开中国多年后,Google 搜索想回来了!
  10. 电脑cpu温度过高怎么办_解决电脑主板CPU温度过高,COC机箱提升电脑健康指数
  11. 分布式缓存技术redis学习系列(四)——redis高级应用(集群搭建、集群分区原理、集群操作)...
  12. 【转】C#正则表达式教程和示例
  13. Lytain:PCWin10纯净专业版重装与程序员的高效部署
  14. ①万字《详解canvas api画图》小白前端入门教程(建议收藏)
  15. Abaqus有限元分析软件介绍
  16. 安卓获取浏览器上网记录_Android 获取自带浏览器上网记录
  17. 异常解决java.lang.ClassNotFoundException: org.springframework.boot.actuate.endpoint.PublicMetrics
  18. 气象接口返回图标_中国天气网API接口
  19. 纯千兆电口和自适应电口的区别
  20. 华为设备配置PoE功能

热门文章

  1. java当前月份减一个月_Java获取当前时间的上一个月和下一个月,第一天和最后一天,任意时间的第一天和最后一天,任意时间上一个月和下一个月...
  2. la环球乐园里的机器人_北京环球度假区发布小黄人乐园主题视频,抢鲜感受未来的欢乐体验...
  3. mysql通过日志恢复数据_mysql通过binlog日志来恢复数据
  4. 360浏览器打不开qq空间_360浏览器打不开? 60浏览器打不开网页的处理方法(图文)...
  5. 设计灵感|网页建议页面(联系页面)版式案例
  6. UI设计中配色专辑素材|做图配色,一键搞定
  7. UI设计素材字体|三明治3D文字效果– 3个角度
  8. idea404未找到错误怎么解决_普联路由器提示无线网密码位数错误怎么解决【解决方法】...
  9. python values count_使用Python Pandas从数据框中获取总values_count
  10. 获取当前图层所处的坐标系统(C++)(ArcObject开发)