zoukankan      html  css  js  c++  java
  • WPF自定义控件与样式(14)-轻量MVVM模式实践

    一.前言

      申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接。

      MVVM是WPF中一个非常实用的编程模式,充分利用了WPF的绑定机制,体现了WPF数据驱动的优势。

     图片来源:(WPF的MVVM

      关于MVVM网上很多介绍或者示例,本文不多做介绍了,本文的主要目的是提供一个轻量级的View Model实现,本文的主要内容:

    • 依赖通知InotifyPropertyChanged实现;
    • 命令Icommand的实现;
    • 消息的实现;
    • 一个简单MVVM示例;

      对于是否要使用MVVM、如何使用,个人觉得根据具体需求可以灵活处理,不用纠结于模式本身。用了MVVM,后置*.cs文件就不一定不允许写任何代码,混合着用也是没有问题的, 只要自己决的方便、代码结构清晰、维护方便即可。

    二.依赖通知InotifyPropertyChanged实现

      依赖通知InotifyPropertyChanged是很简单的一个接口,是View Model标配的接口,一个典型的实现(BaseNotifyPropertyChanged):  

    复制代码
       /// <summary>
        /// 实现了属性更改通知的基类
        /// </summary>
        public class BaseNotifyPropertyChanged : System.ComponentModel.INotifyPropertyChanged
        {
            /// <summary>
            /// 属性值变化时发生
            /// </summary>
            /// <param name="propertyName"></param>
            protected virtual void OnPropertyChanged(string propertyName)
            {
                if (this.PropertyChanged != null)
                    this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }
    
            public virtual event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
        }
    复制代码

      然后使用方式就是这样的:  

    复制代码
            public int _Age;
    
            public int Age
            {
                get { return this._Age; }
                set { this._Age = value; base.OnPropertyChanged("Age"); }
            }
    复制代码

      上面的代码有硬编码,有代码洁癖的人就不爽了,因此网上有多种解决方式,比如这篇:WPF MVVM之INotifyPropertyChanged接口的几种实现方式。本文的实现方式如下,使用表达式树:

    复制代码
            /// <summary>
            /// 属性值变化时发生
            /// </summary>
            /// <param name="propertyName"></param>
            protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
            {
                var propertyName = (propertyExpression.Body as MemberExpression).Member.Name;
                this.OnPropertyChanged(propertyName);
            }
    复制代码

      使用上避免了硬编码,使用示例:  

    复制代码
            public string _Name;
            public string Name
            {
                get { return this._Name; }
                set { this._Name = value; base.OnPropertyChanged(() => this.Name); }
            }
    复制代码

    三.命令Icommand的实现

      命令的实现也很简单,实现Icommand的几个接口就OK了, 考虑到使用时能更加方便,无参数RelayCommand实现:  

    复制代码
        /// <summary>
        /// 广播命令:基本ICommand实现接口
        /// </summary>
        public class RelayCommand : ICommand
        {
            public Action ExecuteCommand { get; private set; }
            public Func<bool> CanExecuteCommand { get; private set; }
    
            public RelayCommand(Action executeCommand, Func<bool> canExecuteCommand)
            {
                this.ExecuteCommand = executeCommand;
                this.CanExecuteCommand = canExecuteCommand;
            }
    
            public RelayCommand(Action executeCommand)
                : this(executeCommand, null) { }
    
            /// <summary>
            /// 定义在调用此命令时调用的方法。
            /// </summary>
            /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>
            public void Execute(object parameter)
            {
                if (this.ExecuteCommand != null) this.ExecuteCommand();
            }
    
            /// <summary>
            /// 定义用于确定此命令是否可以在其当前状态下执行的方法。
            /// </summary>
            /// <returns>
            /// 如果可以执行此命令,则为 true;否则为 false。
            /// </returns>
            /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>
            public bool CanExecute(object parameter)
            {
                return CanExecuteCommand == null || CanExecuteCommand();
            }
    
            public event EventHandler CanExecuteChanged
            {
                add { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested += value; }
                remove { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested -= value; }
            }
        }
    复制代码

      泛型参数RelayCommand<T>的版本:  

    复制代码
        /// <summary>
        /// 广播命令:基本ICommand实现接口,带参数
        /// </summary>
        public class RelayCommand<T> : ICommand
        {
            public Action<T> ExecuteCommand { get; private set; }
    
            public Predicate<T> CanExecuteCommand { get; private set; }
    
            public RelayCommand(Action<T> executeCommand, Predicate<T> canExecuteCommand)
            {
                this.ExecuteCommand = executeCommand;
                this.CanExecuteCommand = canExecuteCommand;
            }
    
            public RelayCommand(Action<T> executeCommand)
                : this(executeCommand, null) { }
    
            /// <summary>
            /// 定义在调用此命令时调用的方法。
            /// </summary>
            /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>
            public void Execute(object parameter)
            {
                if (this.ExecuteCommand != null) this.ExecuteCommand((T)parameter);
            }
    
            /// <summary>
            /// 定义用于确定此命令是否可以在其当前状态下执行的方法。
            /// </summary>
            /// <returns>
            /// 如果可以执行此命令,则为 true;否则为 false。
            /// </returns>
            /// <param name="parameter">此命令使用的数据。如果此命令不需要传递数据,则该对象可以设置为 null。</param>
            public bool CanExecute(object parameter)
            {
                return CanExecuteCommand == null || CanExecuteCommand((T)parameter);
            }
    
            public event EventHandler CanExecuteChanged
            {
                add { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested += value; }
                remove { if (this.CanExecuteCommand != null) CommandManager.RequerySuggested -= value; }
            }
        }
    复制代码

      带参数和不带参数的命令XAML绑定方式:  

    <core:FButton Margin="5 0 0 0" Command="{Binding ShowUserCommand}">ShowUser</core:FButton>
    <core:FButton Margin="5 0 0 0" Command="{Binding SetNameCommand}" FIcon="&#xe60c;"
                              CommandParameter="{Binding Text,ElementName=txtSetName}">SetName</core:FButton>

      上面是针对提供Command模式的控件示例, 但对于其他事件呢,比如MouseOver如何绑定呢?可以借用System.Windows.Interactivity.dll,其中的 Interaction 可以帮助我们实现对命令的绑定,这是在微软Blend中提供的。添加dll应用,然后添加命名空间:

      xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

    复制代码
                <TextBlock VerticalAlignment="Center" Margin="5 0 0 0" Text="MoseOver" x:Name="txbMessage">
                    <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseMove">
                        <i:InvokeCommandAction Command="{Binding MouseOverCommand}" CommandParameter="{Binding ElementName=txbMessage}"></i:InvokeCommandAction>
                    </i:EventTrigger>
                    </i:Interaction.Triggers>
                </TextBlock>
    复制代码

    四.消息的实现

      消息类Messenger主要目的是实现View与View Model及各个模块之间的通信。本文的消息类Messenger,参考自网络开源的实现(MVVMFoundation)。实现了松散耦合的消息通知机制,对于消息传输参数,内部使用了弱引用(WeakReference),以防止内存泄漏代码:  

    复制代码
        /// <summary>
        /// Provides loosely-coupled messaging between various colleague objects.  All references to objects are stored weakly, to prevent memory leaks.
        /// 提供松散耦合的消息通知机制,为防止内存泄漏,所有对象都使用了弱引用(WeakReference)
        /// </summary>
        public class Messenger
        {
            #region Constructor
    
            public Messenger()
            {
            }
    
            #endregion // Constructor
    
            #region Register
    
            /// <summary>
            /// Registers a callback method, with no parameter, to be invoked when a specific message is broadcasted.
            /// 注册消息监听
            /// </summary>
            /// <param name="message">The message to register for.</param>
            /// <param name="callback">The callback to be called when this message is broadcasted.</param>
            public void Register(string message, Action callback)
            {
                this.Register(message, callback, null);
            }
    
            /// <summary>
            /// Registers a callback method, with a parameter, to be invoked when a specific message is broadcasted.
            /// 注册消息监听
            /// </summary>
            /// <param name="message">The message to register for.</param>
            /// <param name="callback">The callback to be called when this message is broadcasted.</param>
            public void Register<T>(string message, Action<T> callback)
            {
                this.Register(message, callback, typeof(T));
            }
    
            void Register(string message, Delegate callback, Type parameterType)
            {
                if (String.IsNullOrEmpty(message))
                    throw new ArgumentException("'message' cannot be null or empty.");
    
                if (callback == null)
                    throw new ArgumentNullException("callback");
    
                this.VerifyParameterType(message, parameterType);
    
                _messageToActionsMap.AddAction(message, callback.Target, callback.Method, parameterType);
            }
    
            [Conditional("DEBUG")]
            void VerifyParameterType(string message, Type parameterType)
            {
                Type previouslyRegisteredParameterType = null;
                if (_messageToActionsMap.TryGetParameterType(message, out previouslyRegisteredParameterType))
                {
                    if (previouslyRegisteredParameterType != null && parameterType != null)
                    {
                        if (!previouslyRegisteredParameterType.Equals(parameterType))
                            throw new InvalidOperationException(string.Format(
                                "The registered action's parameter type is inconsistent with the previously registered actions for message '{0}'.
    Expected: {1}
    Adding: {2}",
                                message,
                                previouslyRegisteredParameterType.FullName,
                                parameterType.FullName));
                    }
                    else
                    {
                        // One, or both, of previouslyRegisteredParameterType or callbackParameterType are null.
                        if (previouslyRegisteredParameterType != parameterType)   // not both null?
                        {
                            throw new TargetParameterCountException(string.Format(
                                "The registered action has a number of parameters inconsistent with the previously registered actions for message "{0}".
    Expected: {1}
    Adding: {2}",
                                message,
                                previouslyRegisteredParameterType == null ? 0 : 1,
                                parameterType == null ? 0 : 1));
                        }
                    }
                }
            }
    
            #endregion // Register
    
            #region Notify
    
            /// <summary>
            /// Notifies all registered parties that a message is being broadcasted.
            /// 发送消息通知,触发监听执行
            /// </summary>
            /// <param name="message">The message to broadcast.</param>
            /// <param name="parameter">The parameter to pass together with the message.</param>
            public void Notify(string message, object parameter)
            {
                if (String.IsNullOrEmpty(message))
                    throw new ArgumentException("'message' cannot be null or empty.");
    
                Type registeredParameterType;
                if (_messageToActionsMap.TryGetParameterType(message, out registeredParameterType))
                {
                    if (registeredParameterType == null)
                        throw new TargetParameterCountException(string.Format("Cannot pass a parameter with message '{0}'. Registered action(s) expect no parameter.", message));
                }
    
                var actions = _messageToActionsMap.GetActions(message);
                if (actions != null)
                    actions.ForEach(action => action.DynamicInvoke(parameter));
            }
    
            /// <summary>
            /// Notifies all registered parties that a message is being broadcasted.
            /// 发送消息通知,触发监听执行
            /// </summary>
            /// <param name="message">The message to broadcast.</param>
            public void Notify(string message)
            {
                if (String.IsNullOrEmpty(message))
                    throw new ArgumentException("'message' cannot be null or empty.");
    
                Type registeredParameterType;
                if (_messageToActionsMap.TryGetParameterType(message, out registeredParameterType))
                {
                    if (registeredParameterType != null)
                        throw new TargetParameterCountException(string.Format("Must pass a parameter of type {0} with this message. Registered action(s) expect it.", registeredParameterType.FullName));
                }
    
                var actions = _messageToActionsMap.GetActions(message);
                if (actions != null)
                    actions.ForEach(action => action.DynamicInvoke());
            }
    
            #endregion // NotifyColleauges
    
            #region MessageToActionsMap [nested class]
    
            /// <summary>
            /// This class is an implementation detail of the Messenger class.
            /// </summary>
            private class MessageToActionsMap
            {
                #region Constructor
    
                internal MessageToActionsMap()
                {
                }
    
                #endregion // Constructor
    
                #region AddAction
    
                /// <summary>
                /// Adds an action to the list.
                /// </summary>
                /// <param name="message">The message to register.</param>
                /// <param name="target">The target object to invoke, or null.</param>
                /// <param name="method">The method to invoke.</param>
                /// <param name="actionType">The type of the Action delegate.</param>
                internal void AddAction(string message, object target, MethodInfo method, Type actionType)
                {
                    if (message == null)
                        throw new ArgumentNullException("message");
    
                    if (method == null)
                        throw new ArgumentNullException("method");
    
                    lock (_map)
                    {
                        if (!_map.ContainsKey(message))
                            _map[message] = new List<WeakAction>();
    
                        _map[message].Add(new WeakAction(target, method, actionType));
                    }
                }
    
                #endregion // AddAction
    
                #region GetActions
    
                /// <summary>
                /// Gets the list of actions to be invoked for the specified message
                /// </summary>
                /// <param name="message">The message to get the actions for</param>
                /// <returns>Returns a list of actions that are registered to the specified message</returns>
                internal List<Delegate> GetActions(string message)
                {
                    if (message == null)
                        throw new ArgumentNullException("message");
    
                    List<Delegate> actions;
                    lock (_map)
                    {
                        if (!_map.ContainsKey(message))
                            return null;
    
                        List<WeakAction> weakActions = _map[message];
                        actions = new List<Delegate>(weakActions.Count);
                        for (int i = weakActions.Count - 1; i > -1; --i)
                        {
                            WeakAction weakAction = weakActions[i];
                            if (weakAction == null)
                                continue;
    
                            Delegate action = weakAction.CreateAction();
                            if (action != null)
                            {
                                actions.Add(action);
                            }
                            else
                            {
                                // The target object is dead, so get rid of the weak action.
                                weakActions.Remove(weakAction);
                            }
                        }
    
                        // Delete the list from the map if it is now empty.
                        if (weakActions.Count == 0)
                            _map.Remove(message);
                    }
    
                    // Reverse the list to ensure the callbacks are invoked in the order they were registered.
                    actions.Reverse();
    
                    return actions;
                }
    
                #endregion // GetActions
    
                #region TryGetParameterType
    
                /// <summary>
                /// Get the parameter type of the actions registered for the specified message.
                /// </summary>
                /// <param name="message">The message to check for actions.</param>
                /// <param name="parameterType">
                /// When this method returns, contains the type for parameters 
                /// for the registered actions associated with the specified message, if any; otherwise, null.
                /// This will also be null if the registered actions have no parameters.
                /// This parameter is passed uninitialized.
                /// </param>
                /// <returns>true if any actions were registered for the message</returns>
                internal bool TryGetParameterType(string message, out Type parameterType)
                {
                    if (message == null)
                        throw new ArgumentNullException("message");
    
                    parameterType = null;
                    List<WeakAction> weakActions;
                    lock (_map)
                    {
                        if (!_map.TryGetValue(message, out weakActions) || weakActions.Count == 0)
                            return false;
                    }
                    parameterType = weakActions[0].ParameterType;
                    return true;
                }
    
                #endregion // TryGetParameterType
    
                #region Fields
    
                // Stores a hash where the key is the message and the value is the list of callbacks to invoke.
                readonly Dictionary<string, List<WeakAction>> _map = new Dictionary<string, List<WeakAction>>();
    
                #endregion // Fields
            }
    
            #endregion // MessageToActionsMap [nested class]
    
            #region WeakAction [nested class]
    
            /// <summary>
            /// This class is an implementation detail of the MessageToActionsMap class.
            /// </summary>
            private class WeakAction
            {
                #region Constructor
    
                /// <summary>
                /// Constructs a WeakAction.
                /// </summary>
                /// <param name="target">The object on which the target method is invoked, or null if the method is static.</param>
                /// <param name="method">The MethodInfo used to create the Action.</param>
                /// <param name="parameterType">The type of parameter to be passed to the action. Pass null if there is no parameter.</param>
                internal WeakAction(object target, MethodInfo method, Type parameterType)
                {
                    if (target == null)
                    {
                        _targetRef = null;
                    }
                    else
                    {
                        _targetRef = new WeakReference(target);
                    }
    
                    _method = method;
    
                    this.ParameterType = parameterType;
    
                    if (parameterType == null)
                    {
                        _delegateType = typeof(Action);
                    }
                    else
                    {
                        _delegateType = typeof(Action<>).MakeGenericType(parameterType);
                    }
                }
    
                #endregion // Constructor
    
                #region CreateAction
    
                /// <summary>
                /// Creates a "throw away" delegate to invoke the method on the target, or null if the target object is dead.
                /// </summary>
                internal Delegate CreateAction()
                {
                    // Rehydrate into a real Action object, so that the method can be invoked.
                    if (_targetRef == null)
                    {
                        return Delegate.CreateDelegate(_delegateType, _method);
                    }
                    else
                    {
                        try
                        {
                            object target = _targetRef.Target;
                            if (target != null)
                                return Delegate.CreateDelegate(_delegateType, target, _method);
                        }
                        catch
                        {
                        }
                    }
    
                    return null;
                }
    
                #endregion // CreateAction
    
                #region Fields
    
                internal readonly Type ParameterType;
    
                readonly Type _delegateType;
                readonly MethodInfo _method;
                readonly WeakReference _targetRef;
    
                #endregion // Fields
            }
    
            #endregion // WeakAction [nested class]
    
            #region Fields
    
            readonly MessageToActionsMap _messageToActionsMap = new MessageToActionsMap();
    
            #endregion // Fields
        }
    复制代码

      在后面的示例中有简单使用。

    五.简单MVVM示例

    5.1 View Model定义实现

      实现一个UserViewModel,定义了两个通知属性,3个命令,用于在XAML中实现不同的命令绑定处理,还注册了一个消息,代码:  

    复制代码
       public class UserViewModel : BaseNotifyPropertyChanged
        {
            public string _Name;
            public string Name
            {
                get { return this._Name; }
                set { this._Name = value; base.OnPropertyChanged(() => this.Name); }
            }
    
            public int _Age;
    
            public int Age
            {
                get { return this._Age; }
                set { this._Age = value; base.OnPropertyChanged("Age"); }
            }
    
            public RelayCommand<string> SetNameCommand { get; private set; }
            public RelayCommand ShowUserCommand { get; private set; }
            public RelayCommand<FrameworkElement> MouseOverCommand { get; private set; }
    
            public UserViewModel()
            {
                this.SetNameCommand = new RelayCommand<string>(this.SetName);
                this.ShowUserCommand = new RelayCommand(this.ShowUser);
                this.MouseOverCommand = new RelayCommand<FrameworkElement>(this.MouseOver);
                Page_MVVM.GlobalMessager.Register("123", () =>
                {
                    MessageBoxX.Info("我是处理123消息的!");
                });
            }
    
            public void SetName(string name)
            {
                if (MessageBoxX.Question(string.Format("要把Name值由[{0}]修改为[{1}]吗?", this.Name, name)))
                {
                    this.Name = name;
                }
            }
    
            public void ShowUser()
            {
                MessageBoxX.Info(this.Name + "---" + this.Age);
            }
    
            public void MouseOver(FrameworkElement tb)
            {
                MessageBoxX.Info("我好像摸到了" + tb.Name);
            }
        }
    复制代码

    5.2 测试页面Page_MVVM.xaml

      创建一个测试页面Page_MVVM,后置代码如下,在构造函数里注入View Model,在一个按钮事件里发送消息:  

    复制代码
        public partial class Page_MVVM : Page
        {
            public static Messenger GlobalMessager = new Messenger();
    
            public Page_MVVM()
            {
                InitializeComponent();
                //set vm
                UserViewModel uvm = new UserViewModel();
                uvm.Name = "kwong";
                uvm.Age = 30;
                this.DataContext = uvm;
    
            }
    
            private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
            {
                GlobalMessager.Notify("123");
            }
        }
    复制代码

      完整XAML代码:  

    复制代码
    <Page x:Class="Kwong.Framework.WPFTest.Page_MVVM"
          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:core="clr-namespace:XLY.Framework.Controls;assembly=XLY.Framework.Controls"
          xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
          mc:Ignorable="d" 
          d:DesignHeight="600" d:DesignWidth="800"
        Title="Page_MVVM">
        <Page.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Height" Value="80"/>
                <Setter Property="Margin" Value="3"/>
                <Setter Property="Orientation" Value="Horizontal"/>
                <Setter Property="Background" Value="{StaticResource WindowBackground}"/>
            </Style>
        </Page.Resources>
        <StackPanel Style="{x:Null}">
            <StackPanel >
                <TextBox Height="30" Width="240" Text="{Binding Name,UpdateSourceTrigger=PropertyChanged}" Margin="5 0 0 0"
                         core:ControlAttachProperty.Label="{Binding Name.Length,Mode=OneWay}" 
                         Style="{StaticResource LabelTextBox}"/>
                <TextBox Height="30" Width="240" Text="{Binding Age}" core:ControlAttachProperty.Label="Age:" 
                         Style="{StaticResource LabelTextBox}" Margin="5 0 0 0"/>
    
            </StackPanel>
            <StackPanel>
                <core:FButton Margin="5 0 0 0" Command="{Binding ShowUserCommand}">ShowUser</core:FButton>
                <core:FButton Margin="5 0 0 0" FIcon="&#xe61c;" Width="125" Click="ButtonBase_OnClick">Send Message</core:FButton>
            </StackPanel>
            <StackPanel>
                <TextBox Height="30" Width="240" x:Name="txtSetName"  core:ControlAttachProperty.Label="Name-" Margin="5 0 0 0"
                         Style="{StaticResource LabelTextBox}"></TextBox>
                <core:FButton Margin="5 0 0 0" Command="{Binding SetNameCommand}" FIcon="&#xe60c;"
                              CommandParameter="{Binding Text,ElementName=txtSetName}">SetName</core:FButton>
            </StackPanel>
            <StackPanel>
                <TextBlock VerticalAlignment="Center" Margin="5 0 0 0" Text="MoseOver" x:Name="txbMessage">
                    <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseMove">
                        <i:InvokeCommandAction Command="{Binding MouseOverCommand}" CommandParameter="{Binding ElementName=txbMessage}"></i:InvokeCommandAction>
                    </i:EventTrigger>
                    </i:Interaction.Triggers>
                </TextBlock>
            </StackPanel>
        </StackPanel>
    </Page>
    复制代码

    5.3 效果

     

     附录:参考引用

    WPF自定义控件与样式(1)-矢量字体图标(iconfont)

    WPF自定义控件与样式(2)-自定义按钮FButton

    WPF自定义控件与样式(3)-TextBox & RichTextBox & PasswordBox样式、水印、Label标签、功能扩展

    WPF自定义控件与样式(4)-CheckBox/RadioButton自定义样式

    WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展

    WPF自定义控件与样式(6)-ScrollViewer与ListBox自定义样式

    WPF自定义控件与样式(7)-列表控件DataGrid与ListView自定义样式

    WPF自定义控件与样式(8)-ComboBox与自定义多选控件MultComboBox

    WPF自定义控件与样式(9)-树控件TreeView与菜单Menu-ContextMenu

    WPF自定义控件与样式(10)-进度控件ProcessBar自定义样 

    WPF自定义控件与样式(11)-等待/忙/正在加载状态-控件实现

    WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表

    WPF自定义控件与样式(13)-自定义窗体Window & 自适应内容大小消息框MessageBox

    版权所有,文章来源:http://www.cnblogs.com/anding

  • 相关阅读:
    C#对SQLite、Access数据库操作的封装,很好用的~
    如何使用MFC连接Access数据库
    字节、十六进制字符串相互转换(asc2hex、hex2asc)
    Oracle数据库模式关系和数据备份导出导入
    Oracle数据库sql常用
    Oracle数据库的函数,存储过程,程序包,游标,触发器
    收集一下
    JS 获取随机颜色值
    IDEA webapp文件夹不识别解决方案
    使用postman请求响应Invalid CORS request
  • 原文地址:https://www.cnblogs.com/ljdong7/p/12115251.html
Copyright © 2011-2022 走看看