diff --git a/QYMessageCenter.Business/MessageBusiness.cs b/QYMessageCenter.Business/MessageBusiness.cs index 9fcd4b4..c322493 100644 --- a/QYMessageCenter.Business/MessageBusiness.cs +++ b/QYMessageCenter.Business/MessageBusiness.cs @@ -1,8 +1,11 @@ using BBWYB.Server.Business; using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using QYMessageCenter.Common.Extensions; using QYMessageCenter.Common.Http; using QYMessageCenter.Common.Log; using QYMessageCenter.Common.Models; +using QYMessageCenter.Model.DB; using QYMessageCenter.Model.DTO; using System.Configuration; using System.Threading.Channels; @@ -25,12 +28,16 @@ namespace QYMessageCenter.Business /// public void Send(SendMessageRequest request) { + var msg = request.Map(); + msg.Id = idGenerator.NewLong(); + msg.CreateTime = DateTime.Now; + #region 调用goeasy var goeasy_httpResult = restApiService.SendRequest("https://rest-hz.goeasy.io/", "v2/pubsub/publish", new { appkey = Key, channel = request.Channel, - content = request.Content + content = JsonConvert.SerializeObject(msg) }, null, HttpMethod.Post); if (goeasy_httpResult.StatusCode != System.Net.HttpStatusCode.OK) diff --git a/QYMessageCenter.Client/App.xaml b/QYMessageCenter.Client/App.xaml index 4f02c61..96f5eb9 100644 --- a/QYMessageCenter.Client/App.xaml +++ b/QYMessageCenter.Client/App.xaml @@ -2,8 +2,14 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:QYMessageCenter.Client" + xmlns:ctr="clr-namespace:QYMessageCenter.Client.Converters" StartupUri="MainWindow.xaml"> - + + + + + + diff --git a/QYMessageCenter.Client/App.xaml.cs b/QYMessageCenter.Client/App.xaml.cs index fca2011..67d7ca2 100644 --- a/QYMessageCenter.Client/App.xaml.cs +++ b/QYMessageCenter.Client/App.xaml.cs @@ -24,6 +24,8 @@ namespace QYMessageCenter.Client #if DEBUG //齐越山鸡 userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxNTM1MzMwMzI4ODkyMTQ5NzYwIiwidGVhbUlkIjoiMTUxNjk3NDI1MDU0MjUwMTg4OCIsInNvblRlYW1JZHMiOiIxNDM2Mjg4NTAwMjM1MjQzNTIwIiwiZXhwIjoxNzI2MzAwNjY0fQ.hPSbgJEuTt0MLy_7YkSJX4rRG3drJAfso-5IS8ZlOkY"; + + //userToken="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMzk1NzA4MjA4NjU1MzcyMjg4IiwidGVhbUlkIjoiMTQzNjI4ODUwMDIzNTI0MzUyMCIsInNvblRlYW1JZHMiOiIxNDM2Mjg4NTAwMjM1MjQzNTIwIiwiZXhwIjoxNzQwMTM3NzY5fQ.Wq7IRvAkrVpn28Bj3ImG_KAg9MZ3C2Ux84vSjXyd0ZY"; //测试 #else var tokenResult = ReadMMF(); diff --git a/QYMessageCenter.Client/Converters/AppCodeConverter.cs b/QYMessageCenter.Client/Converters/AppCodeConverter.cs new file mode 100644 index 0000000..5762650 --- /dev/null +++ b/QYMessageCenter.Client/Converters/AppCodeConverter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace QYMessageCenter.Client.Converters +{ + public class AppCodeConverter : IValueConverter + { + private IDictionary codeDic; + + public AppCodeConverter() + { + codeDic = new Dictionary() + { + { "PJZS", "评价助手" }, + { "BBWYC", "步步为盈C端" }, + { "BBWYB", "步步为盈B端" }, + { "QK", "齐库" }, + { "LK", "良库" }, + { "SBF", "三板斧" }, + { "SN", "司南" }, + }; + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var appCode = value?.ToString() ?? string.Empty; + if (codeDic.TryGetValue(appCode, out var appName)) + return appName; + else return "UnKnow App"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/QYMessageCenter.Client/GlobalContext.cs b/QYMessageCenter.Client/GlobalContext.cs index ec732b3..046df36 100644 --- a/QYMessageCenter.Client/GlobalContext.cs +++ b/QYMessageCenter.Client/GlobalContext.cs @@ -1,6 +1,11 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using QYMessageCenter.Client.Models; +using QYMessageCenter.Client.Models.Msg; +using QYMessageCenter.Common.Extensions; using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; namespace QYMessageCenter.Client { @@ -8,6 +13,16 @@ namespace QYMessageCenter.Client [ComVisible(true)] public class GlobalContext { + public GlobalContext() + { + messageHandleDic = new Dictionary>() + { + { "PJZS", OnReceiveMessageFromPJZS } + }; + + popupManager = new PopupManager(); + } + public User User { get; set; } public string UserToken { get; set; } @@ -16,5 +31,95 @@ namespace QYMessageCenter.Client { return JsonConvert.SerializeObject(User); } + + + private IDictionary> messageHandleDic; + + private PopupManager popupManager; + + public void OnError(string msg) + { + App.Current.Dispatcher.Invoke(() => + { + MessageBox.Show(msg, "齐越消息中心"); + }); + } + + public void OnReceiveMessage(string channel, string message) + { + try + { + var msg = JsonConvert.DeserializeObject(message); + + if (!string.IsNullOrEmpty(msg.RecevierId) && !msg.RecevierId.Contains(User.Id.ToString())) + return; //忽略不是自己的消息 + if (messageHandleDic.ContainsKey(msg.AppCode)) + messageHandleDic[msg.AppCode](msg); + else + OnReceiveMessageFromUnknow(msg); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "齐越消息中心"); + } + } + + private void OnReceiveMessageFromPJZS(Message msg) + { + if (msg.CustomTypeCode == "SHANGJIASHIBAI") + { + var smsg = msg.Map(); + var j = JObject.Parse(msg.Content); + + smsg.ActivityName = j.Value("ActivityName"); + smsg.SpuLogo = j.Value("SpuLogo"); + smsg.MainProductSpu = j.Value("MainProductSpu"); + smsg.ErrorMsg = j.Value("ErrorMsg"); + + App.Current.Dispatcher.BeginInvoke(() => popupManager.Show(smsg)); + return; + } + + App.Current.Dispatcher.BeginInvoke(() => popupManager.Show(msg)); + } + + private void OnReceiveMessageFromUnknow(Message msg) + { + //ReceiveMessageList.Insert(0, msg); + App.Current.Dispatcher.BeginInvoke(() => popupManager.Show(msg)); + } + + public void Test() + { + App.Current.Dispatcher.BeginInvoke((Action)delegate + { + popupManager.Show(new Message_PJZS_SHANGJIASHIBAI() + { + AppCode = "PJZS", + ShopId = "12899501", + CustomTypeCode = "SHANGJIASHIBAI", + Title = "信奉玩具专营店", + Content = JsonConvert.SerializeObject(new + { + + }), + ActivityName = "小熊头枕腰靠", + MainProductSpu = "10025305065298", + SpuLogo = "https://img13.360buyimg.com/n9/s100x100_jfs/t1/235072/18/12198/94918/65b261a6F6de6ba33/23b803a34546427f.jpg", + ErrorMsg = "上架sku失败-com,jd.bk,saf.exception.SafJosException:销售属性排序冲冲突,同一顺序下不能有两个相同别名,别名:[柿柿如意发财果串-乔迁吊坠]顺序:[3]#8d306b629bae458faa58e9f5f0c883fb(Solution reference: https://jos.jd.com/commondoc?listld=171)" + }); + }); + + App.Current.Dispatcher.BeginInvoke((Action)delegate + { + popupManager.Show(new Message() + { + AppCode = "PJZS", + ShopId = "12899501", + Title = "信奉玩具专营店", + Content = "上架sku失败-com,jd.bk,saf.exception.SafJosException:这是一个普通模板消息", + }); + }); + } } } diff --git a/QYMessageCenter.Client/MainWindow.xaml b/QYMessageCenter.Client/MainWindow.xaml index 495cb2e..128945e 100644 --- a/QYMessageCenter.Client/MainWindow.xaml +++ b/QYMessageCenter.Client/MainWindow.xaml @@ -6,7 +6,8 @@ xmlns:local="clr-namespace:QYMessageCenter.Client" mc:Ignorable="d" ShowInTaskbar="False" - Title="齐越消息中心" Height="450" Width="800"> + WindowStartupLocation="CenterScreen" + Title="齐越消息中心" Height="100" Width="300"> diff --git a/QYMessageCenter.Client/MainWindow.xaml.cs b/QYMessageCenter.Client/MainWindow.xaml.cs index 49c8b25..61e7f7b 100644 --- a/QYMessageCenter.Client/MainWindow.xaml.cs +++ b/QYMessageCenter.Client/MainWindow.xaml.cs @@ -7,6 +7,7 @@ using QYMessageCenter.Common.Extensions; using System.IO; using System.Reflection; using System.Text; +using System.Transactions; using System.Windows; using io = System.IO; namespace QYMessageCenter.Client @@ -45,17 +46,29 @@ namespace QYMessageCenter.Client var wb2Setting = CoreWebView2Environment.CreateAsync(userDataFolder: io.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "QYMessageCenter.Client")).Result; wb2.CoreWebView2InitializationCompleted += Wb2_CoreWebView2InitializationCompleted; await wb2.EnsureCoreWebView2Async(wb2Setting); + wb2.CoreWebView2.PermissionRequested += CoreWebView2_PermissionRequested; //wb2.CoreWebView2.NavigateToString(html); } + private void CoreWebView2_PermissionRequested(object? sender, CoreWebView2PermissionRequestedEventArgs e) + { + e.State = CoreWebView2PermissionState.Allow; + } + private void Wb2_CoreWebView2InitializationCompleted(object? sender, CoreWebView2InitializationCompletedEventArgs e) { Login(); wb2.CoreWebView2.AddHostObjectToScript("qymsgcenter", this.globalContext); - var html = File.ReadAllText(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "static", "index.html"), Encoding.UTF8); - wb2.CoreWebView2.NavigateToString(html); + //var html = File.ReadAllText(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "static", "index.html"), Encoding.UTF8); + //wb2.CoreWebView2.NavigateToString(html); + + var htmlPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "static", "index.html"); + wb2.CoreWebView2.Navigate(htmlPath); + this.Visibility = Visibility.Collapsed; + + //globalContext.Test(); } private void Login() diff --git a/QYMessageCenter.Client/Models/Msg/Message.cs b/QYMessageCenter.Client/Models/Msg/Message.cs new file mode 100644 index 0000000..8a15403 --- /dev/null +++ b/QYMessageCenter.Client/Models/Msg/Message.cs @@ -0,0 +1,71 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace QYMessageCenter.Client.Models +{ + public class Message : ObservableObject + { + private string appCode; + private string title; + private string content; + + + public long Id { get; set; } + + /// + /// 消息频道 + /// + public string Channel { get; set; } + + /// + /// 消息所属团队Id + /// + public string TeamId { get; set; } + + /// + /// 消息所属店铺Id + /// + public string ShopId { get; set; } + + public DateTime? CreateTime { get; set; } + + /// + /// 自定义类型编码 + /// + public string CustomTypeCode { get; set; } + + /// + /// 已读人员 + /// + public string ReaderId { get; set; } + + /// + /// 发送人 + /// + public string SenderId { get; set; } + + /// + /// 接收人(可空) + /// + public string RecevierId { get; set; } + + /// + /// 是否为Json消息, 解析规则参考CustomTypeCode的约定 + /// + public bool IsJsonMsg { get; set; } + + /// + /// 消息所属应用 + /// + public string AppCode { get => appCode; set { SetProperty(ref appCode, value); } } + + /// + /// 消息标题 + /// + public string Title { get => title; set { SetProperty(ref title, value); } } + + /// + /// 消息内容 + /// + public string Content { get => content; set { SetProperty(ref content, value); } } + } +} diff --git a/QYMessageCenter.Client/Models/Msg/Message_PJZS_SHANGJIASHIBAI.cs b/QYMessageCenter.Client/Models/Msg/Message_PJZS_SHANGJIASHIBAI.cs new file mode 100644 index 0000000..643cbe0 --- /dev/null +++ b/QYMessageCenter.Client/Models/Msg/Message_PJZS_SHANGJIASHIBAI.cs @@ -0,0 +1,25 @@ +namespace QYMessageCenter.Client.Models.Msg +{ + /// + /// 消息_评价助手_上架失败 + /// + public class Message_PJZS_SHANGJIASHIBAI : Message + { + + private string activityName; + + private string spuLogo; + + private string mainProductSpu; + + private string errorMsg; + + public string ActivityName { get => activityName; set { SetProperty(ref activityName, value); } } + + public string SpuLogo { get => spuLogo; set { SetProperty(ref spuLogo, value); } } + + public string MainProductSpu { get => mainProductSpu; set { SetProperty(ref mainProductSpu, value); } } + + public string ErrorMsg { get => errorMsg; set { SetProperty(ref errorMsg, value); } } + } +} diff --git a/QYMessageCenter.Client/PopupManager.cs b/QYMessageCenter.Client/PopupManager.cs new file mode 100644 index 0000000..2022cec --- /dev/null +++ b/QYMessageCenter.Client/PopupManager.cs @@ -0,0 +1,64 @@ +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; +using QYMessageCenter.Client.Models; + +namespace QYMessageCenter.Client +{ + public class PopupManager + { + private List MessageList { get; set; } + + + private PopupWindow pw; + + public PopupManager() + { + MessageList = new List(); + WeakReferenceMessenger.Default.Register(this, (o, x) => ShowNext()); + } + + public void Show(Message msg) + { + MessageList.Insert(0, msg); + Show(); + } + + private void Show() + { + var msg = MessageList[0]; + if (pw != null) + { + pw.RefreshMsg(msg); + } + else + { + pw = new PopupWindow(msg); + pw.Closed += Pw_Closed; + pw.Show(); + } + } + + private void Pw_Closed(object? sender, EventArgs e) + { + pw = null; + MessageList.Clear(); + } + + public void ShowNext() + { + if (MessageList.Count() > 0) + MessageList.RemoveAt(0); + if (MessageList.Count() == 0) + pw.Close(); + else + Show(); + } + } + + public class MVVMMessage_ShowNext : ValueChangedMessage + { + public MVVMMessage_ShowNext(object value) : base(value) + { + } + } +} diff --git a/QYMessageCenter.Client/PopupWindow.xaml b/QYMessageCenter.Client/PopupWindow.xaml new file mode 100644 index 0000000..cc459ce --- /dev/null +++ b/QYMessageCenter.Client/PopupWindow.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QYMessageCenter.Client/PopupWindow.xaml.cs b/QYMessageCenter.Client/PopupWindow.xaml.cs new file mode 100644 index 0000000..30b0875 --- /dev/null +++ b/QYMessageCenter.Client/PopupWindow.xaml.cs @@ -0,0 +1,47 @@ +using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Extensions.DependencyInjection; +using QYMessageCenter.Client.Models; +using SJ.Controls; +using System.Windows; + +namespace QYMessageCenter.Client +{ + /// + /// PopupWindow.xaml 的交互逻辑 + /// + public partial class PopupWindow : BWindow + { + private Message msg; + public Message Msg { get => msg; set { Set(ref msg, value); } } + + public PopupWindow(Message msg) + { + InitializeComponent(); + this.Msg = msg; + this.DataContext = this; + this.Loaded += PopupWindow_Loaded; + } + + private void PopupWindow_Loaded(object sender, RoutedEventArgs e) + { + //弹窗显示在右下角 + this.Left = SystemParameters.WorkArea.Width - this.Width; + this.Top = SystemParameters.WorkArea.Height - this.Height; + } + + public void RefreshMsg(Message msg) + { + this.Msg = msg; + } + + private void btn_showNext_Click(object sender, RoutedEventArgs e) + { + WeakReferenceMessenger.Default.Send(new MVVMMessage_ShowNext(null)); + } + + private void btn_Close_Click(object sender, RoutedEventArgs e) + { + WeakReferenceMessenger.Default.Send(new MVVMMessage_ShowNext(null)); + } + } +} diff --git a/QYMessageCenter.Client/QYMessageCenter.Client.csproj b/QYMessageCenter.Client/QYMessageCenter.Client.csproj index 8f44ee7..79e7024 100644 --- a/QYMessageCenter.Client/QYMessageCenter.Client.csproj +++ b/QYMessageCenter.Client/QYMessageCenter.Client.csproj @@ -19,11 +19,13 @@ + + diff --git a/QYMessageCenter.Client/Resources/Themes/Generic.xaml b/QYMessageCenter.Client/Resources/Themes/Generic.xaml new file mode 100644 index 0000000..1a8a628 --- /dev/null +++ b/QYMessageCenter.Client/Resources/Themes/Generic.xaml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/QYMessageCenter.Client/TemplateSelector/MessageTemplateSelector.cs b/QYMessageCenter.Client/TemplateSelector/MessageTemplateSelector.cs new file mode 100644 index 0000000..fc692f7 --- /dev/null +++ b/QYMessageCenter.Client/TemplateSelector/MessageTemplateSelector.cs @@ -0,0 +1,30 @@ +using QYMessageCenter.Client.Models; +using System.Windows; +using System.Windows.Controls; + +namespace QYMessageCenter.Client.TemplateSelector +{ + public class MessageTemplateSelector : DataTemplateSelector + { + public DataTemplate Template_Normal { get; set; } + + public DataTemplate Template_PJZS_SHANGJIASHIBAI { get; set; } + + public override DataTemplate SelectTemplate(object item, DependencyObject container) + { + if (item == null) + return null; + + var msg = item as Message; + if (msg.AppCode == "PJZS") + { + if (msg.CustomTypeCode == "SHANGJIASHIBAI") + { + return Template_PJZS_SHANGJIASHIBAI; + } + } + + return Template_Normal; + } + } +} diff --git a/QYMessageCenter.Client/static/index.html b/QYMessageCenter.Client/static/index.html index 9150140..97be09f 100644 --- a/QYMessageCenter.Client/static/index.html +++ b/QYMessageCenter.Client/static/index.html @@ -4,52 +4,71 @@ - + - + \ No newline at end of file diff --git a/QYMessageCenter.Model/DB/QYNotification.cs b/QYMessageCenter.Model/DB/QYNotification.cs index 88c488b..38adfd8 100644 --- a/QYMessageCenter.Model/DB/QYNotification.cs +++ b/QYMessageCenter.Model/DB/QYNotification.cs @@ -4,7 +4,7 @@ namespace QYMessageCenter.Model.DB { [Table(Name = "qynotification", DisableSyncStructure = true)] - public partial class Qynotification + public partial class QYNotification { [Column(DbType = "bigint", IsPrimary = true)] @@ -34,6 +34,12 @@ namespace QYMessageCenter.Model.DB [Column(StringLength = 50)] public string ShopId { get; set; } + /// + /// 标题 + /// + [Column(StringLength = 100)] + public string Title { get; set; } + /// /// 消息内容 /// diff --git a/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs b/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs index 5d777f5..ab3e30c 100644 --- a/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs +++ b/QYMessageCenter.Model/DTO/Message/SendMessageRequest.cs @@ -27,6 +27,11 @@ namespace QYMessageCenter.Model.DTO /// public string ShopId { get; set; } + /// + /// 消息标题 + /// + public string Title { get; set; } + /// /// 消息内容 /// diff --git a/QYMessageCenter.sln b/QYMessageCenter.sln index ef4ae38..7d596d8 100644 --- a/QYMessageCenter.sln +++ b/QYMessageCenter.sln @@ -15,7 +15,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{F23D88 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Client", "Client", "{DFE55D4D-6A43-4158-AB78-7B36D0DB351B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QYMessageCenter.Client", "QYMessageCenter.Client\QYMessageCenter.Client.csproj", "{D5998DAB-6517-42EB-A604-BF88C189F1B4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QYMessageCenter.Client", "QYMessageCenter.Client\QYMessageCenter.Client.csproj", "{D5998DAB-6517-42EB-A604-BF88C189F1B4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SJ.Controls", "SJ.Controls\SJ.Controls.csproj", "{D05F067C-90F5-42EF-808B-47FCA5B5E4FE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,6 +45,10 @@ Global {D5998DAB-6517-42EB-A604-BF88C189F1B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5998DAB-6517-42EB-A604-BF88C189F1B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5998DAB-6517-42EB-A604-BF88C189F1B4}.Release|Any CPU.Build.0 = Release|Any CPU + {D05F067C-90F5-42EF-808B-47FCA5B5E4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D05F067C-90F5-42EF-808B-47FCA5B5E4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D05F067C-90F5-42EF-808B-47FCA5B5E4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D05F067C-90F5-42EF-808B-47FCA5B5E4FE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -52,6 +58,7 @@ Global {17CB9C4E-4008-4E5A-98B2-40B666922CE1} = {F23D8876-9ED2-4EC1-9856-0E6AC3E58072} {91F9B6F7-EB22-42AE-80A9-0FF427B055A6} = {F23D8876-9ED2-4EC1-9856-0E6AC3E58072} {D5998DAB-6517-42EB-A604-BF88C189F1B4} = {DFE55D4D-6A43-4158-AB78-7B36D0DB351B} + {D05F067C-90F5-42EF-808B-47FCA5B5E4FE} = {DFE55D4D-6A43-4158-AB78-7B36D0DB351B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F8628884-5171-4D3A-9A40-9C788CBA7EEB} diff --git a/QYMessageCenter/SWDescription.txt b/QYMessageCenter/SWDescription.txt index 2af620a..4d5a41d 100644 --- a/QYMessageCenter/SWDescription.txt +++ b/QYMessageCenter/SWDescription.txt @@ -15,5 +15,6 @@ SN 司南 Channel规范 -CustomTypeCode说明 -评价助手: +需要特殊处理的CustomTypeCode说明 +评价助手 +SHANGJIASHIBAI 上架失败 diff --git a/SJ.Controls/AssemblyInfo.cs b/SJ.Controls/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/SJ.Controls/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/SJ.Controls/BAsyncImage.cs b/SJ.Controls/BAsyncImage.cs new file mode 100644 index 0000000..c98346b --- /dev/null +++ b/SJ.Controls/BAsyncImage.cs @@ -0,0 +1,705 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Schedulers; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Resources; +using dw = System.Drawing; + +namespace SJ.Controls +{ + public class BAsyncImage : Control + { + #region DependencyProperty + public static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register("DecodePixelWidth", + typeof(double), typeof(BAsyncImage), new PropertyMetadata(0.0)); + + public static readonly DependencyProperty LoadingTextProperty = + DependencyProperty.Register("LoadingText", typeof(string), typeof(BAsyncImage), new PropertyMetadata("Loading...")); + + public static readonly DependencyProperty IsLoadingProperty = + DependencyProperty.Register("IsLoading", typeof(bool), typeof(BAsyncImage), new PropertyMetadata(false)); + + public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(BAsyncImage)); + + public static readonly DependencyProperty DefaultUrlSourceProperty = DependencyProperty.Register("DefaultUrlSource", typeof(string), typeof(BAsyncImage), new PropertyMetadata("")); + + public static readonly DependencyProperty UrlSourceProperty = + DependencyProperty.Register("UrlSource", typeof(string), typeof(BAsyncImage), new PropertyMetadata(string.Empty, new PropertyChangedCallback((s, e) => + { + var asyncImg = s as BAsyncImage; + if (asyncImg.LoadEventFlag) + { + asyncImg.Load(); + } + }))); + + public static readonly DependencyProperty FailUrlSourceProperty = + DependencyProperty.Register("FailUrlSource", typeof(string), typeof(BAsyncImage), new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty IsCacheProperty = DependencyProperty.Register("IsCache", typeof(bool), typeof(BAsyncImage), new PropertyMetadata(true)); + + public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(BAsyncImage), new PropertyMetadata(Stretch.Uniform)); + + public static readonly DependencyProperty CacheGroupProperty = DependencyProperty.Register("CacheGroup", typeof(string), typeof(BAsyncImage), new PropertyMetadata("QLAsyncImage_Default")); + #endregion + + #region Property + private static readonly HttpClient httpClient = new HttpClient(); + + public double StaticImageActualPixelWidth { get; set; } = 0; + + public double StaticImageActualPixelHeight { get; set; } = 0; + + public const string LocalRegex = @"^([C-J]):\\([^:&]+\\)*([^:&]+).(jpg|jpeg|png|gif)$"; + public const string HttpRegex = @"^((https|http):\/\/)?([^\\*+@]+)$"; + + private Image _image; + private dw.Bitmap gifBitmap; + private bool LoadEventFlag = false; + private static ConcurrentDictionary ImageCacheList; + private static ConcurrentDictionary GifImageCacheList; + + private static LimitedConcurrencyLevelTaskScheduler httpTaskScheduler; + + public double DecodePixelWidth + { + get { return (double)GetValue(DecodePixelWidthProperty); } + set { SetValue(DecodePixelWidthProperty, value); } + } + public string LoadingText + { + get { return GetValue(LoadingTextProperty) as string; } + set { SetValue(LoadingTextProperty, value); } + } + + public bool IsLoading + { + get { return (bool)GetValue(IsLoadingProperty); } + set { SetValue(IsLoadingProperty, value); } + } + + public string UrlSource + { + get { return GetValue(UrlSourceProperty) as string; } + set { SetValue(UrlSourceProperty, value); } + } + + /// + /// 仅限Resourcesl路径 + /// + public string FailUrlSource + { + get { return GetValue(FailUrlSourceProperty) as string; } + set { SetValue(FailUrlSourceProperty, value); } + } + + public ImageSource ImageSource + { + get { return GetValue(ImageSourceProperty) as ImageSource; } + set { SetValue(ImageSourceProperty, value); } + } + + public bool IsCache + { + get { return (bool)GetValue(IsCacheProperty); } + set { SetValue(IsCacheProperty, value); } + } + + public Stretch Stretch + { + get { return (Stretch)GetValue(StretchProperty); } + set { SetValue(StretchProperty, value); } + } + + public string CacheGroup + { + get { return GetValue(CacheGroupProperty) as string; } + set { SetValue(CacheGroupProperty, value); } + } + + #endregion + + #region RouteEvent + public delegate void QLAsyncImageLoadCompleteHandler(object sender, QLAsyncImageLoadCompleteEventArgs e); + + public static readonly RoutedEvent OnLoadCompleteEvent = EventManager.RegisterRoutedEvent("OnLoadComplete", RoutingStrategy.Bubble, typeof(QLAsyncImageLoadCompleteHandler), typeof(BAsyncImage)); + + public event QLAsyncImageLoadCompleteHandler OnLoadComplete + { + add { AddHandler(OnLoadCompleteEvent, value); } + remove { RemoveHandler(OnLoadCompleteEvent, value); } + } + #endregion + + #region Method + static BAsyncImage() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(BAsyncImage), new FrameworkPropertyMetadata(typeof(BAsyncImage))); + ImageCacheList = new ConcurrentDictionary(); + GifImageCacheList = new ConcurrentDictionary(); + httpTaskScheduler = new LimitedConcurrencyLevelTaskScheduler(10); + } + + public BAsyncImage() + { + Loaded += QLAsyncImage_Loaded; + Unloaded += QLAsyncImage_Unloaded; + } + + private void QLAsyncImage_Unloaded(object sender, RoutedEventArgs e) + { + Reset(); + LoadEventFlag = false; + } + + private void QLAsyncImage_Loaded(object sender, RoutedEventArgs e) + { + Init(); + } + + public override void OnApplyTemplate() + { + Init(); + } + + private void Init([CallerMemberName] string eventName = "") + { + if (LoadEventFlag) + return; + + _image = GetTemplateChild("image") as Image; + + if (_image == null) + return; + + LoadEventFlag = true; + Load(); + } + + /// + /// Delete local bitmap resource + /// Reference: http://msdn.microsoft.com/en-us/library/dd183539(VS.85).aspx + /// + [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool DeleteObject(IntPtr hObject); + + /// + /// 重置图像 + /// + private void Reset() + { + //停止播放Gif动画 + if (gifBitmap != null) + StopGif(); + SetSource(null); + } + + private void Load() + { + if (_image == null) + return; + + Reset(); + + if (!string.IsNullOrEmpty(UrlSource)) + { + var url = UrlSource; + var failUrl = FailUrlSource; + var pixelWidth = (int)DecodePixelWidth; + var isCache = IsCache; + var cacheKey = string.Format("{0}_{1}", CacheGroup, url); + IsLoading = !ImageCacheList.ContainsKey(cacheKey) && !GifImageCacheList.ContainsKey(cacheKey); + + #region 读取缓存 + if (ImageCacheList.ContainsKey(cacheKey)) + { + var source = ImageCacheList[cacheKey]; + SetSource(source); + SetStaticImageActualPixelSize(source.Width, source.Height); + LoadComplete(string.Empty, url); + return; + } + else if (GifImageCacheList.ContainsKey(cacheKey)) + { + PlayGif(GifImageCacheList[cacheKey]); + LoadComplete(string.Empty, url); + return; + } + #endregion + + this.Load(url, failUrl, cacheKey, isCache, pixelWidth); + } + } + + private void Load(string url, string failUrl, string cacheKey, bool isCache, int pixelWidth) + { + var errorMessage = string.Empty; + + //解析路径类型 + var pathType = ValidatePathType(url); + if (pathType == PathType.Invalid) + { + LoadFail(failUrl); + LoadComplete(errorMessage, url); + return; + } + + if (pathType == PathType.Http) + { + ////先加载默认图 + //if (!string.IsNullOrEmpty(defaultUrl)) + // LoadLocal(defaultUrl, failUrl, PathType.Resources, defaultUrl, true, 0, excuteComplete: false); //默认图不触发加载完毕事件 + LoadHttp(url, failUrl, cacheKey, isCache, pixelWidth); + } + else + LoadLocal(url, failUrl, pathType, cacheKey, isCache, pixelWidth); + } + + /// + /// 加载失败图像 + /// + /// + private void LoadFail(string failUrl) + { + byte[] imgBytes = null; + string errorMessage = string.Empty; + var pathType = ValidatePathType(failUrl); + if (pathType == PathType.Invalid) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"LoadFail 无效的路径 {failUrl}"); + Console.ResetColor(); + return; + } + if (pathType == PathType.Local) + imgBytes = LoadBytesFromLocal(failUrl, out errorMessage); + else if (pathType == PathType.Resources) + imgBytes = LoadBytesFromApplicationResource(failUrl, out errorMessage); + + + if (string.IsNullOrEmpty(errorMessage) && imgBytes != null) + AnalysisBytes(imgBytes, failUrl, true, 0, out errorMessage); + + if (string.IsNullOrEmpty(errorMessage)) + return; + + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"LoadFail {errorMessage} {failUrl}"); + Console.ResetColor(); + } + + private void LoadLocal(string url, string failUrl, PathType pathType, string cacheKey, bool isCache, int pixelWidth, bool excuteComplete = true) + { + byte[] imgBytes = null; + var errorMessage = string.Empty; + if (pathType == PathType.Local) + imgBytes = LoadBytesFromLocal(url, out errorMessage); + else if (pathType == PathType.Resources) + imgBytes = LoadBytesFromApplicationResource(url, out errorMessage); + + if (string.IsNullOrEmpty(errorMessage) && imgBytes != null) + AnalysisBytes(imgBytes, cacheKey, isCache, pixelWidth, out errorMessage); + + if (!string.IsNullOrEmpty(errorMessage)) + { + LoadFail(failUrl); + Console.ForegroundColor = ConsoleColor.Red; + } + Console.WriteLine($"LoadLocal {errorMessage} {url}"); + Console.ResetColor(); + if (excuteComplete) + LoadComplete(errorMessage, url); + return; + } + + private void LoadHttp(string url, string failUrl, string cacheKey, bool isCache, int pixelWidth) + { + Task.Factory.StartNew(() => + { + //Thread.Sleep(2000); + Console.WriteLine($"LoadHttp Start {url}"); + var errorMessage = string.Empty; + var imgBytes = LoadBytesFromHttp(url, out errorMessage); + + if (string.IsNullOrEmpty(errorMessage) && imgBytes != null) + AnalysisBytes(imgBytes, cacheKey, isCache, pixelWidth, out errorMessage); + + if (!string.IsNullOrEmpty(errorMessage)) + { + LoadFail(failUrl); + Console.ForegroundColor = ConsoleColor.Red; + } + Console.WriteLine($"LoadHttp Completed {errorMessage} {url}"); + Console.ResetColor(); + LoadComplete(errorMessage, url); + return; + }, CancellationToken.None, TaskCreationOptions.None, httpTaskScheduler); + } + + private void AnalysisBytes(byte[] imgBytes, string cacheKey, bool isCache, int pixelWidth, out string errorMessage) + { + errorMessage = string.Empty; + + #region 读取文件类型 + var imgType = GetImageType(imgBytes); + if (imgType == ImageType.Invalid) + { + imgBytes = null; + errorMessage = "Invalid ImageFile"; + return; + } + #endregion + + #region 加载图像 + if (imgType != ImageType.Gif) + { + //加载静态图像 + var imgSource = LoadStaticImage(cacheKey, imgBytes, pixelWidth, isCache, out errorMessage); + if (imgSource == null) + return; + SetStaticImageActualPixelSize(imgSource.Width, imgSource.Height); + SetSource(imgSource); + } + else + { + var frameCount = 0; + using (var memoryStream = new System.IO.MemoryStream(imgBytes)) + { + //读取gif帧数 + var decoder = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); + frameCount = decoder.Frames.Count; + decoder = null; + } + if (frameCount > 1) + { + CacheGifBytes(cacheKey, imgBytes, isCache); + PlayGif(imgBytes); + } + else + { + //gif只有1帧,视为静态图处理 + var imgSource = LoadStaticImage(cacheKey, imgBytes, pixelWidth, isCache, out errorMessage); + if (imgSource == null) + return; + SetStaticImageActualPixelSize(imgSource.Width, imgSource.Height); + SetSource(imgSource); + } + } + #endregion + } + + + /// + /// 加载静态图像 + /// + /// + /// + /// + /// + /// + private ImageSource LoadStaticImage(string cacheKey, byte[] imgBytes, int pixelWidth, bool isCache, out string errorMessage) + { + errorMessage = string.Empty; + if (ImageCacheList.ContainsKey(cacheKey)) + return ImageCacheList[cacheKey]; + var bit = new BitmapImage() { CacheOption = BitmapCacheOption.OnLoad }; + try + { + bit.BeginInit(); + if (pixelWidth != 0) + { + bit.DecodePixelWidth = pixelWidth; + } + bit.StreamSource = new System.IO.MemoryStream(imgBytes); + bit.EndInit(); + bit.Freeze(); + if (isCache && !ImageCacheList.ContainsKey(cacheKey)) + ImageCacheList.TryAdd(cacheKey, bit); + } + catch (Exception ex) + { + errorMessage = $"LoadStaticImage Error {ex.Message}"; + bit = null; + } + return bit; + } + + /// + /// 加载Gif图像动画 + /// + /// + /// + /// + /// + /// + private void CacheGifBytes(string cacheKey, byte[] imgBytes, bool isCache) + { + if (isCache && !GifImageCacheList.ContainsKey(cacheKey)) + GifImageCacheList.TryAdd(cacheKey, imgBytes); + } + + private byte[] LoadBytesFromHttp(string url, out string errorMessage) + { + errorMessage = string.Empty; + try + { + return httpClient.GetByteArrayAsync(url).Result; + } + catch (Exception ex) + { + errorMessage = $"Dowdload Error {ex.Message}"; + } + return null; + } + + private byte[] LoadBytesFromLocal(string path, out string errorMessage) + { + errorMessage = string.Empty; + if (!System.IO.File.Exists(path)) + { + errorMessage = "File No Exists"; + return null; + } + try + { + return System.IO.File.ReadAllBytes(path); + } + catch (Exception ex) + { + errorMessage = $"Load Local Error {ex.Message}"; + return null; + } + } + + private byte[] LoadBytesFromApplicationResource(string path, out string errorMessage) + { + errorMessage = string.Empty; + try + { + StreamResourceInfo streamInfo = Application.GetResourceStream(new Uri(path, UriKind.RelativeOrAbsolute)); + if (streamInfo.Stream.CanRead) + { + using (streamInfo.Stream) + { + var bytes = new byte[streamInfo.Stream.Length]; + streamInfo.Stream.Read(bytes, 0, bytes.Length); + return bytes; + } + } + } + catch (Exception ex) + { + errorMessage = $"Load Resource Error {ex.Message}"; + return null; + } + return null; + } + + private void SetSource(ImageSource source) + { + Dispatcher.BeginInvoke((Action)delegate + { + if (_image != null) + _image.Source = source; + }); + } + + /// + /// 更新图像实际像素 + /// + /// + private void SetStaticImageActualPixelSize(double pixelWidth, double pixelHeight) + { + Dispatcher.Invoke(() => + { + StaticImageActualPixelWidth = pixelWidth; + StaticImageActualPixelHeight = pixelHeight; + }); + } + + private void PlayGif(byte[] imgBytes) + { + gifBitmap = new dw.Bitmap(new System.IO.MemoryStream(imgBytes)); + if (dw.ImageAnimator.CanAnimate(gifBitmap)) + { + SetStaticImageActualPixelSize(gifBitmap.Width, gifBitmap.Height); + dw.ImageAnimator.Animate(gifBitmap, OnGifFrameChanged); + } + else + { + gifBitmap.Dispose(); + } + } + + private void StopGif() + { + dw.ImageAnimator.StopAnimate(gifBitmap, OnGifFrameChanged); + gifBitmap.Dispose(); + } + + private void OnGifFrameChanged(object sender, EventArgs e) + { + dw.ImageAnimator.UpdateFrames(); + var currentFrameImageSource = GetBitmapSource(); + if (currentFrameImageSource != null) + currentFrameImageSource.Freeze(); + SetSource(currentFrameImageSource); + } + + /// + /// 加载完成 + /// + /// + /// + private void LoadComplete(string errorMessage, string url) + { + this.Dispatcher.BeginInvoke((Action)delegate + { + IsLoading = false; + var args = new QLAsyncImageLoadCompleteEventArgs(OnLoadCompleteEvent, this) + { + IsSuccess = string.IsNullOrEmpty(errorMessage), + UrlSource = url, + ErrorMessage = errorMessage, + StaticImageActualPixelWidth = StaticImageActualPixelWidth, + StaticImageActualPixelHeight = StaticImageActualPixelHeight + }; + this.RaiseEvent(args); + }); + } + + /// + /// 从System.Drawing.Bitmap中获得当前帧图像的BitmapSource + /// + /// + private ImageSource GetBitmapSource() + { + IntPtr handle = IntPtr.Zero; + try + { + handle = gifBitmap.GetHbitmap(); + return Imaging.CreateBitmapSourceFromHBitmap(handle, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); + } + catch + { + return null; + } + finally + { + if (handle != IntPtr.Zero) + { + DeleteObject(handle); + } + } + } + + public void Refresh() + { + Load(); + } + + private PathType ValidatePathType(string path) + { + if (path.StartsWith("pack://")) + return PathType.Resources; + else if (Regex.IsMatch(path, BAsyncImage.LocalRegex, RegexOptions.IgnoreCase)) + return PathType.Local; + else if (Regex.IsMatch(path, BAsyncImage.HttpRegex, RegexOptions.IgnoreCase)) + return PathType.Http; + else + return PathType.Invalid; + } + + private ImageType GetImageType(byte[] bytes) + { + var type = ImageType.Invalid; + try + { + var fileHead = Convert.ToInt32($"{bytes[0]}{bytes[1]}"); + if (!Enum.IsDefined(typeof(ImageType), fileHead)) + { + type = ImageType.Invalid; + } + else + { + type = (ImageType)fileHead; + } + } + catch (Exception ex) + { + type = ImageType.Invalid; + Console.WriteLine($"获取图片类型失败 {ex.Message}"); + } + return type; + } + + /// + /// 清楚缓存 + /// + /// 缓存Key,格式: CacheGroup_UrlSource + public static void ClearCache(string cacheKey = "") + { + if (string.IsNullOrEmpty(cacheKey)) + { + ImageCacheList.Clear(); + GifImageCacheList.Clear(); + return; + } + + ImageCacheList.Remove(cacheKey, out _); + GifImageCacheList.Remove(cacheKey, out _); + } + + public static ImageSource GetImageCache(string cacheKey) + { + if (ImageCacheList.ContainsKey(cacheKey)) + return ImageCacheList[cacheKey]; + return null; + } + #endregion + } + + + public enum PathType + { + Invalid = 0, Local = 1, Http = 2, Resources = 3 + } + + public enum ImageType + { + Invalid = 0, Gif = 7173, Jpg = 255216, Png = 13780, Bmp = 6677 + } + + public class QLAsyncImageLoadCompleteEventArgs : RoutedEventArgs + { + public QLAsyncImageLoadCompleteEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { } + + public bool IsSuccess { get; set; } + + public string UrlSource { get; set; } + + public string ErrorMessage { get; set; } + + /// + /// 当加载静态图时的实际像素宽度 + /// + public double StaticImageActualPixelWidth { get; set; } + + /// + /// 当加载静态图时的实际像素高度 + /// + public double StaticImageActualPixelHeight { get; set; } + } +} diff --git a/SJ.Controls/BButton.cs b/SJ.Controls/BButton.cs new file mode 100644 index 0000000..a0c18ac --- /dev/null +++ b/SJ.Controls/BButton.cs @@ -0,0 +1,72 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace SJ.Controls +{ + [StyleTypedProperty(Property = "Style", StyleTargetType = typeof(BButton))] + public class BButton : Button + { + static BButton() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(BButton), new FrameworkPropertyMetadata(typeof(BButton))); + } + + public static readonly DependencyProperty BorderCornerRadiusProperty = DependencyProperty.Register("BorderCornerRadius", typeof(CornerRadius), typeof(BButton)); + public static readonly DependencyProperty MouseOverBgColorProperty = DependencyProperty.Register("MouseOverBgColor", typeof(Brush), typeof(BButton)); + public static readonly DependencyProperty MouseOverFontColorProperty = DependencyProperty.Register("MouseOverFontColor", typeof(Brush), typeof(BButton)); + public static readonly DependencyProperty PressedBgColorProperty = DependencyProperty.Register("PressedBgColor", typeof(Brush), typeof(BButton)); + public static readonly DependencyProperty PressedFontColorProperty = DependencyProperty.Register("PressedFontColor", typeof(Brush), typeof(BButton)); + public static readonly DependencyProperty DisableBgColorProperty = DependencyProperty.Register("DisableBgColor", typeof(Brush), typeof(BButton)); + public static readonly DependencyProperty DisableTextProperty = DependencyProperty.Register("DisableText", typeof(string), typeof(BButton)); + public static readonly DependencyProperty PressedScaleProperty = DependencyProperty.Register("PressedScale", typeof(bool), typeof(BButton), new PropertyMetadata(true)); + + public CornerRadius BorderCornerRadius + { + get { return (CornerRadius)GetValue(BorderCornerRadiusProperty); } + set { SetValue(BorderCornerRadiusProperty, value); } + } + + public Brush MouseOverBgColor + { + get { return GetValue(MouseOverBgColorProperty) as Brush; } + set { SetValue(MouseOverBgColorProperty, value); } + } + + public Brush MouseOverFontColor + { + get { return GetValue(MouseOverFontColorProperty) as Brush; } + set { SetValue(MouseOverFontColorProperty, value); } + } + + public Brush PressedBgColor + { + get { return GetValue(PressedBgColorProperty) as Brush; } + set { SetValue(PressedBgColorProperty, value); } + } + + public Brush PressedFontColor + { + get { return GetValue(PressedFontColorProperty) as Brush; } + set { SetValue(PressedFontColorProperty, value); } + } + + public Brush DisableBgColor + { + get { return GetValue(DisableBgColorProperty) as System.Windows.Media.Brush; } + set { SetValue(DisableBgColorProperty, value); } + } + + public string DisableText + { + get { return GetValue(DisableTextProperty).ToString(); } + set { SetValue(DisableTextProperty, value); } + } + + public bool PressedScale + { + get { return (bool)GetValue(PressedScaleProperty); } + set { SetValue(PressedScaleProperty, value); } + } + } +} diff --git a/SJ.Controls/BTextBox.cs b/SJ.Controls/BTextBox.cs new file mode 100644 index 0000000..67d0e18 --- /dev/null +++ b/SJ.Controls/BTextBox.cs @@ -0,0 +1,210 @@ +using System.Text; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace SJ.Controls +{ + [StyleTypedProperty(Property = "Style", StyleTargetType = typeof(BTextBox))] + public class BTextBox : TextBox + { + #region Property + + private bool IsResponseChange; + private StringBuilder PasswordBuilder; + private int lastOffset; + + #endregion + + #region DependencyProperty + + public static readonly DependencyProperty WaterRemarkProperty = DependencyProperty.Register("WaterRemark", typeof(string), typeof(BTextBox)); + public static readonly DependencyProperty WaterRemarkFontColorProperty = DependencyProperty.Register("WaterRemarkFontColor", typeof(Brush), typeof(BTextBox)); + public static readonly DependencyProperty BorderCornerRadiusProperty = DependencyProperty.Register("BorderCornerRadius", typeof(CornerRadius), typeof(BTextBox)); + public static readonly DependencyProperty IsPasswordBoxProperty = DependencyProperty.Register("IsPasswordBox", typeof(bool), typeof(BTextBox), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsPasswordBoxChnage))); + public static readonly DependencyProperty IsNumberBoxProperty = DependencyProperty.Register("IsNumberBox", typeof(bool), typeof(BTextBox), new PropertyMetadata(false, new PropertyChangedCallback(OnIsNumberBoxChnage))); + public static readonly DependencyProperty DisableBgColorProperty = DependencyProperty.Register("DisableBgColor", typeof(Brush), typeof(BTextBox)); + public static readonly DependencyProperty PasswordCharProperty = DependencyProperty.Register("PasswordChar", typeof(char), typeof(BTextBox), new FrameworkPropertyMetadata('●')); + public static readonly DependencyProperty PasswordStrProperty = DependencyProperty.Register("PasswordStr", typeof(string), typeof(BTextBox), new FrameworkPropertyMetadata(string.Empty, new PropertyChangedCallback(OnPasswordStrChanged))); + + /// + /// 水印文字 + /// + public string WaterRemark + { + get { return GetValue(WaterRemarkProperty).ToString(); } + set { SetValue(WaterRemarkProperty, value); } + } + + public Brush WaterRemarkFontColor + { + get { return GetValue(WaterRemarkFontColorProperty) as Brush; } + set { SetValue(WaterRemarkFontColorProperty, value); } + } + + /// + /// 边框角度 + /// + public CornerRadius BorderCornerRadius + { + get { return (CornerRadius)GetValue(BorderCornerRadiusProperty); } + set { SetValue(BorderCornerRadiusProperty, value); } + } + + /// + /// 是否为密码框 + /// + public bool IsPasswordBox + { + get { return (bool)GetValue(IsPasswordBoxProperty); } + set { SetValue(IsPasswordBoxProperty, value); } + } + + /// + /// 是否为数字框 + /// + public bool IsNumberBox + { + get { return (bool)GetValue(IsNumberBoxProperty); } + set { SetValue(IsNumberBoxProperty, value); } + } + + /// + /// 替换明文的密码字符 + /// + public char PasswordChar + { + get { return (char)GetValue(PasswordCharProperty); } + set { SetValue(PasswordCharProperty, value); } + } + + /// + /// 密码字符串 + /// + public string PasswordStr + { + get + { + var value = GetValue(PasswordStrProperty); + return value == null ? string.Empty : value.ToString(); + } + set { SetValue(PasswordStrProperty, value); } + } + + /// + /// 按钮被禁用时的背景颜色 + /// + public Brush DisableBgColor + { + get { return GetValue(DisableBgColorProperty) as Brush; } + set { SetValue(DisableBgColorProperty, value); } + } + #endregion + + + static BTextBox() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(BTextBox), new FrameworkPropertyMetadata(typeof(BTextBox))); + } + + public BTextBox() + { + IsResponseChange = true; + PasswordBuilder = new StringBuilder(); + this.Loaded += QLTextBox_Loaded; + } + + private void QLTextBox_Loaded(object sender, RoutedEventArgs e) + { + if (IsPasswordBox && !string.IsNullOrEmpty(PasswordStr) && PasswordStr.Length > 0) + { + OnPasswordStrChanged(); + } + } + + private static void OnPasswordStrChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + (d as BTextBox).OnPasswordStrChanged(); + } + + private void OnPasswordStrChanged() + { + if (!IsResponseChange) + return; + IsResponseChange = false; + this.Text = ConvertToPasswordChar(PasswordStr.Length); + IsResponseChange = true; + } + + private static void OnIsPasswordBoxChnage(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + (sender as BTextBox).SetPwdEvent(); + } + + private static void OnIsNumberBoxChnage(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + (sender as BTextBox).SetValidationNumberEvent(); + } + + /// + /// 定义TextChange事件 + /// + private void SetPwdEvent() + { + if (IsPasswordBox) + this.TextChanged += QLTextBox_TextChanged; + else + this.TextChanged -= QLTextBox_TextChanged; + } + + private void SetValidationNumberEvent() + { + if (IsNumberBox) + this.PreviewTextInput += QLTextBox_PreviewTextInput; + else + this.PreviewTextInput -= QLTextBox_PreviewTextInput; + } + + private void QLTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + if (!IsResponseChange) + return; + IsResponseChange = false; + foreach (TextChange c in e.Changes) + { + PasswordStr = PasswordStr.Remove(c.Offset, c.RemovedLength); + PasswordStr = PasswordStr.Insert(c.Offset, Text.Substring(c.Offset, c.AddedLength)); + lastOffset = c.Offset; + } + /*将文本转换为密码字符*/ + this.Text = ConvertToPasswordChar(Text.Length); + IsResponseChange = true; + this.SelectionStart = lastOffset + 1; + } + + private void QLTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) + { + Regex re = new Regex("[^0-9.]+"); + e.Handled = re.IsMatch(e.Text); + } + + + /// + /// 按照指定的长度生成密码字符 + /// + /// + /// + private string ConvertToPasswordChar(int length) + { + if (PasswordBuilder != null) + PasswordBuilder.Clear(); + else + PasswordBuilder = new StringBuilder(); + for (var i = 0; i < length; i++) + PasswordBuilder.Append(PasswordChar); + return PasswordBuilder.ToString(); + } + } +} diff --git a/SJ.Controls/BTextBoxAnimation.cs b/SJ.Controls/BTextBoxAnimation.cs new file mode 100644 index 0000000..7457ffc --- /dev/null +++ b/SJ.Controls/BTextBoxAnimation.cs @@ -0,0 +1,109 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace SJ.Controls +{ + [StyleTypedProperty(Property = "Style", StyleTargetType = typeof(BTextBoxAnimation))] + public class BTextBoxAnimation : BTextBox + { + public static readonly DependencyProperty WaterRemarkTopStateColorProperty = DependencyProperty.Register("WaterRemarkTopStateColor", typeof(Brush), typeof(BTextBoxAnimation)); + public static readonly DependencyProperty WaterRemarkStateProperty = DependencyProperty.Register("WaterRemarkState", typeof(WaterRemarkState), typeof(BTextBoxAnimation), new PropertyMetadata(WaterRemarkState.Normal, new PropertyChangedCallback((d, e) => + { + if (e.OldValue != e.NewValue) + { + (d as BTextBoxAnimation).PlayWaterRemarkAnimation(); + } + }))); + + private TextBlock txtRemark; + private TimeSpan animationTimeSpan = new TimeSpan(0, 0, 0, 0, 200); + private IEasingFunction animationEasingFunction = new PowerEase() { EasingMode = EasingMode.EaseInOut }; + + + public Brush WaterRemarkTopStateColor + { + get { return GetValue(WaterRemarkTopStateColorProperty) as Brush; } + set { SetValue(WaterRemarkTopStateColorProperty, value); } + } + + public WaterRemarkState WaterRemarkState + { + get { return (WaterRemarkState)Convert.ToInt32(GetValue(WaterRemarkStateProperty)); } + set { SetValue(WaterRemarkStateProperty, value); } + } + + + static BTextBoxAnimation() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(BTextBoxAnimation), new FrameworkPropertyMetadata(typeof(BTextBoxAnimation))); + } + public BTextBoxAnimation() + { + this.Loaded += QLTextBoxAnimation_Loaded; + } + + public override void OnApplyTemplate() + { + txtRemark = GetTemplateChild("txtRemark") as TextBlock; + base.OnApplyTemplate(); + } + + private void QLTextBoxAnimation_Loaded(object sender, RoutedEventArgs e) + { + if (!string.IsNullOrEmpty(Text)) + WaterRemarkState = WaterRemarkState.Top; + } + + protected override void OnTextChanged(TextChangedEventArgs e) + { + base.OnTextChanged(e); + if (!this.IsLoaded) + return; + if (string.IsNullOrEmpty(Text)) + WaterRemarkState = WaterRemarkState.Normal; + else + WaterRemarkState = WaterRemarkState.Top; + } + + protected override void OnGotFocus(RoutedEventArgs e) + { + base.OnGotFocus(e); + WaterRemarkState = WaterRemarkState.Top; + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + if (string.IsNullOrEmpty(Text)) + WaterRemarkState = WaterRemarkState.Normal; + } + + private void PlayWaterRemarkAnimation() + { + var fontsize = WaterRemarkState == WaterRemarkState.Normal ? FontSize : 10.5; + var row = WaterRemarkState == WaterRemarkState.Normal ? 1 : 0; + + var storyboard = new Storyboard(); + var daukf_Remark_FontSize = new DoubleAnimationUsingKeyFrames(); + daukf_Remark_FontSize.KeyFrames.Add(new EasingDoubleKeyFrame(fontsize, animationTimeSpan, animationEasingFunction)); + Storyboard.SetTargetProperty(daukf_Remark_FontSize, new PropertyPath("(TextBlock.FontSize)")); + Storyboard.SetTarget(daukf_Remark_FontSize, txtRemark); + storyboard.Children.Add(daukf_Remark_FontSize); + + var i32aukf_Remark_Row = new Int32AnimationUsingKeyFrames(); + i32aukf_Remark_Row.KeyFrames.Add(new EasingInt32KeyFrame(row, animationTimeSpan, animationEasingFunction)); + Storyboard.SetTargetProperty(i32aukf_Remark_Row, new PropertyPath("(Grid.Row)")); + Storyboard.SetTarget(i32aukf_Remark_Row, txtRemark); + storyboard.Children.Add(i32aukf_Remark_Row); + storyboard.Begin(); + } + } + + public enum WaterRemarkState + { + Normal, Top + } +} diff --git a/SJ.Controls/BWindow.cs b/SJ.Controls/BWindow.cs new file mode 100644 index 0000000..9cec0b1 --- /dev/null +++ b/SJ.Controls/BWindow.cs @@ -0,0 +1,178 @@ +using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shell; + +namespace SJ.Controls +{ + [StyleTypedProperty(Property = "Style", StyleTargetType = typeof(BWindow))] + public class BWindow : Window, INotifyPropertyChanged + { + public static readonly DependencyProperty CornerRadiusProperty = + DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(BWindow), new PropertyMetadata(new CornerRadius(0))); + + public static readonly DependencyProperty RightButtonGroupMarginProperty = + DependencyProperty.Register("RightButtonGroupMargin", typeof(Thickness), typeof(BWindow), new PropertyMetadata(new Thickness(0, 16, 16, 0))); + + public static readonly DependencyProperty CloseButtonVisibilityProperty = + DependencyProperty.Register("CloseButtonVisibility", typeof(Visibility), typeof(BWindow), new PropertyMetadata(Visibility.Visible)); + + public static readonly DependencyProperty MinButtonVisibilityProperty = + DependencyProperty.Register("MinButtonVisibility", typeof(Visibility), typeof(BWindow), new PropertyMetadata(Visibility.Visible)); + + public static readonly DependencyProperty MaxButtonVisibilityProperty = + DependencyProperty.Register("MaxButtonVisibility", typeof(Visibility), typeof(BWindow), new PropertyMetadata(Visibility.Visible)); + + public static readonly DependencyProperty CloseButtonColorProperty = + DependencyProperty.Register("CloseButtonColor", typeof(Brush), typeof(BWindow), new PropertyMetadata(new SolidColorBrush(Colors.White))); + + public static readonly DependencyProperty MinButtonColorProperty = + DependencyProperty.Register("MinButtonColor", typeof(Brush), typeof(BWindow), new PropertyMetadata(new SolidColorBrush(Colors.White))); + + public static readonly DependencyProperty MaxButtonColorProperty = + DependencyProperty.Register("MaxButtonColor", typeof(Brush), typeof(BWindow), new PropertyMetadata(new SolidColorBrush(Colors.White))); + + public CornerRadius CornerRadius + { + get { return (CornerRadius)GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + + public Thickness RightButtonGroupMargin + { + get { return (Thickness)GetValue(RightButtonGroupMarginProperty); } + set { SetValue(RightButtonGroupMarginProperty, value); } + } + + public Visibility CloseButtonVisibility + { + get { return (Visibility)GetValue(CloseButtonVisibilityProperty); } + set { SetValue(CloseButtonVisibilityProperty, value); } + } + + public Visibility MinButtonVisibility + { + get { return (Visibility)GetValue(MinButtonVisibilityProperty); } + set { SetValue(MinButtonVisibilityProperty, value); } + } + + public Visibility MaxButtonVisibility + { + get { return (Visibility)GetValue(MaxButtonVisibilityProperty); } + set { SetValue(MaxButtonVisibilityProperty, value); } + } + + public Brush CloseButtonColor + { + get { return (Brush)GetValue(CloseButtonColorProperty); } + set { SetValue(CloseButtonColorProperty, value); } + } + + public Brush MinButtonColor + { + get { return (Brush)GetValue(MinButtonColorProperty); } + set { SetValue(MinButtonColorProperty, value); } + } + + public Brush MaxButtonColor + { + get { return (Brush)GetValue(MaxButtonColorProperty); } + set { SetValue(MaxButtonColorProperty, value); } + } + + static BWindow() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(BWindow), new FrameworkPropertyMetadata(typeof(BWindow))); + } + + public BWindow() + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + var chrome = new WindowChrome + { + CornerRadius = new CornerRadius(), + GlassFrameThickness = new Thickness(1), + UseAeroCaptionButtons = false, + NonClientFrameEdges = NonClientFrameEdges.None, + ResizeBorderThickness = new Thickness(2), + CaptionHeight = 30 + }; + WindowChrome.SetWindowChrome(this, chrome); + } + + public override void OnApplyTemplate() + { + Button PART_MIN = null; + Button PART_MAX = null; + Button PART_RESTORE = null; + Button PART_CLOSE = null; + + PART_MIN = GetTemplateChild("PART_MIN") as Button; + PART_MAX = GetTemplateChild("PART_MAX") as Button; + PART_RESTORE = GetTemplateChild("PART_RESTORE") as Button; + PART_CLOSE = GetTemplateChild("PART_CLOSE") as Button; + + if (PART_RESTORE != null) + PART_RESTORE.Click += PART_RESTORE_Click; + if (PART_MAX != null) + PART_MAX.Click += PART_MAX_Click; + if (PART_MIN != null) + PART_MIN.Click += PART_MIN_Click; + if (PART_CLOSE != null) + PART_CLOSE.Click += PART_CLOSE_Click; + + base.OnApplyTemplate(); + } + + private void PART_CLOSE_Click(object sender, RoutedEventArgs e) + { + this.Close(); + } + + private void PART_MIN_Click(object sender, RoutedEventArgs e) + { + WindowState = WindowState.Minimized; + } + + private void PART_MAX_Click(object sender, RoutedEventArgs e) + { + WindowState = WindowState.Maximized; + } + + private void PART_RESTORE_Click(object sender, RoutedEventArgs e) + { + WindowState = WindowState.Normal; + } + + /// + /// 判断是否为模态窗口 + /// + /// + public bool IsModal() + { + var filedInfo = typeof(Window).GetField("_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic); + return filedInfo != null && (bool)filedInfo.GetValue(this); + } + + + #region PropertyNotify + public event PropertyChangedEventHandler PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + protected bool Set(ref T oldValue, T newValue, [CallerMemberName] string propertyName = "") + { + if (Equals(oldValue, newValue)) + return false; + oldValue = newValue; + OnPropertyChanged(propertyName); + return true; + } + #endregion + } +} diff --git a/SJ.Controls/Extensions/VisualTreeExtension.cs b/SJ.Controls/Extensions/VisualTreeExtension.cs new file mode 100644 index 0000000..e6adc3e --- /dev/null +++ b/SJ.Controls/Extensions/VisualTreeExtension.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; + +namespace SJ.Controls.Extensions +{ + public static class VisualTreeExtension + { + public static T HitTest(this FrameworkElement fe, Point? point) where T : FrameworkElement + { + if (point == null) + point = Mouse.GetPosition(fe); + var result = VisualTreeHelper.HitTest(fe, point.Value); + if (result == null) + return null; + if (result.VisualHit != null) + { + var r = FindParentOfType(result.VisualHit); + return r; + } + return null; + } + + /// + /// 查找父控件 + /// + /// 父控件类型 + /// 子控件实例 + /// + public static T FindParentOfType(this DependencyObject obj) where T : FrameworkElement + { + DependencyObject parent = VisualTreeHelper.GetParent(obj); + while (parent != null) + { + if (parent is T) + { + return (T)parent; + } + parent = VisualTreeHelper.GetParent(parent); + } + return null; + } + + /// + /// 查找子控件 + /// + /// 需要查找的控件类型 + /// 父控件实例 + /// + public static T FindFirstVisualChild(this DependencyObject obj) where T : FrameworkElement + { + var queue = new Queue(); + queue.Enqueue(obj); + while (queue.Count > 0) + { + DependencyObject current = queue.Dequeue(); + for (int i = VisualTreeHelper.GetChildrenCount(current) - 1; 0 <= i; i--) + { + DependencyObject child = VisualTreeHelper.GetChild(current, i); + if (child != null && child is T) + { + return (T)child; + } + queue.Enqueue(child); + } + } + return null; + } + } +} diff --git a/SJ.Controls/Helpers/StoryboardHelper.cs b/SJ.Controls/Helpers/StoryboardHelper.cs new file mode 100644 index 0000000..ba92b60 --- /dev/null +++ b/SJ.Controls/Helpers/StoryboardHelper.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Media.Animation; + +namespace SJ.Controls.Helpers +{ + public class StoryboardHelper + { + /// + /// 播放动画 + /// + /// 控件源 + /// 开始值 + /// 结束值 + /// 时间间隔 + /// 是否反向播放 + /// 是否重复播放 + /// 动画类型 + /// 回调函数 + /// 动画属性 + public static void _PlayDoubleAnimation(FrameworkElement fe, double from, double to, TimeSpan duration, bool IsAutoReverse, bool RepeatPlay, IEasingFunction ef, Action Callback, string PropertyPath) + { + Storyboard _sb = new Storyboard(); + _sb.Completed += new EventHandler((s, e) => + { + if (Callback != null) + Callback(); + _sb.Stop(); + _sb.Children.Clear(); + _sb = null; + }); + DoubleAnimation daRotation = new DoubleAnimation(); + daRotation.From = from; + daRotation.To = to; + daRotation.EasingFunction = ef; + daRotation.Duration = duration; + Storyboard.SetTargetProperty(daRotation, new PropertyPath(PropertyPath)); + Storyboard.SetTarget(daRotation, fe); + _sb.Children.Add(daRotation); + _sb.AutoReverse = IsAutoReverse; + if (RepeatPlay) + _sb.RepeatBehavior = RepeatBehavior.Forever; + _sb.Begin(); + } + + public static void _PlayAnimationUsingKeyFrames(IList AnimationUsingKeyFrameList, bool IsAutoReverse, bool IsRepeayPlay, Action Callback) + { + if (AnimationUsingKeyFrameList == null || AnimationUsingKeyFrameList.Count == 0) + return; + Storyboard _sb = new Storyboard(); + _sb.Completed += new EventHandler((s, e) => + { + if (Callback != null) + Callback(); + _sb.Stop(); + _sb.Children.Clear(); + _sb = null; + }); + _sb.AutoReverse = IsAutoReverse; + if (IsRepeayPlay) + _sb.RepeatBehavior = RepeatBehavior.Forever; + foreach (AnimationModel am in AnimationUsingKeyFrameList) + { + AnimationTimeline animationTimeLine = null; + switch (am._KeyFrameType) + { + case KeyFrameType.DoubleKeyFrame: + animationTimeLine = CreateDoubleAnimationUsingKeyFrames(am); + break; + case KeyFrameType.ColorKeyFrame: + animationTimeLine = CreateColorAnimationUsingKeyFrames(am); + break; + case KeyFrameType.ObjectKeyFrame: + animationTimeLine = CreateObjectAnimationUsingKeyFrames(am); + break; + } + _sb.Children.Add(animationTimeLine); + } + _sb.Begin(); + } + + private static AnimationTimeline CreateDoubleAnimationUsingKeyFrames(AnimationModel am) + { + DoubleAnimationUsingKeyFrames animationTimeline = new DoubleAnimationUsingKeyFrames(); + Storyboard.SetTargetProperty(animationTimeline, new PropertyPath(am.PropertyPath)); + Storyboard.SetTarget(animationTimeline, am.Element); + foreach (BaseKeyFrame baseKeyFrame in am.KeyFrames) + { + animationTimeline.KeyFrames.Add( + new EasingDoubleKeyFrame( + Convert.ToInt32(baseKeyFrame.Value), + baseKeyFrame._KeyTime, + baseKeyFrame.EasingFunction) + ); + } + return animationTimeline; + } + + private static AnimationTimeline CreateColorAnimationUsingKeyFrames(AnimationModel am) + { + ColorAnimationUsingKeyFrames animationTimeline = new ColorAnimationUsingKeyFrames(); + Storyboard.SetTargetProperty(animationTimeline, new PropertyPath(am.PropertyPath)); + Storyboard.SetTarget(animationTimeline, am.Element); + foreach (BaseKeyFrame baseKeyFrame in am.KeyFrames) + { + animationTimeline.KeyFrames.Add( + new EasingColorKeyFrame( + (System.Windows.Media.Color)baseKeyFrame.Value, + baseKeyFrame._KeyTime, + baseKeyFrame.EasingFunction) + ); + } + return animationTimeline; + } + + private static AnimationTimeline CreateObjectAnimationUsingKeyFrames(AnimationModel am) + { + ObjectAnimationUsingKeyFrames animationTimeline = new ObjectAnimationUsingKeyFrames(); + Storyboard.SetTargetProperty(animationTimeline, new PropertyPath(am.PropertyPath)); + Storyboard.SetTarget(animationTimeline, am.Element); + foreach (BaseKeyFrame baseKeyFrame in am.KeyFrames) + { + animationTimeline.KeyFrames.Add( + new DiscreteObjectKeyFrame( + baseKeyFrame.Value, + baseKeyFrame._KeyTime) + ); + } + return animationTimeline; + } + } + + /// + /// 关键帧动画类型 + /// + public enum KeyFrameType + { + DoubleKeyFrame = 1, + ColorKeyFrame = 2, + ObjectKeyFrame = 3 + } + + public class AnimationModel + { + public AnimationModel() + { + this.KeyFrames = new List(); + } + /// + /// 执行动画的对象 + /// + public FrameworkElement Element; + + /// + /// 作用于动画的属性 + /// + public string PropertyPath; + + /// + /// 动画类型枚举 + /// + public KeyFrameType _KeyFrameType; + + /// + /// 关键帧动画帧集合 + /// + public IList KeyFrames; + } + + public class BaseKeyFrame + { + /// + /// 动画触发时间 + /// + public TimeSpan _KeyTime; + + /// + /// 值 + /// + public object Value; + + /// + /// 缓动函数类型 + /// + public IEasingFunction EasingFunction; + } +} diff --git a/SJ.Controls/PageControl.xaml b/SJ.Controls/PageControl.xaml new file mode 100644 index 0000000..dc79179 --- /dev/null +++ b/SJ.Controls/PageControl.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/SJ.Controls/PageControl.xaml.cs b/SJ.Controls/PageControl.xaml.cs new file mode 100644 index 0000000..6bc2c2a --- /dev/null +++ b/SJ.Controls/PageControl.xaml.cs @@ -0,0 +1,137 @@ +using System; +using System.Windows; +using System.Windows.Controls; + +namespace SJ.Controls +{ + /// + /// PageControl.xaml 的交互逻辑 + /// + public partial class PageControl : UserControl + { + public PageControl() + { + InitializeComponent(); + this.Loaded += PageControl_Loaded; + } + + private void PageControl_Loaded(object sender, RoutedEventArgs e) + { + this.PageIndex = 1; + pageArgs.PageIndex = this.PageIndex; + } + + /// + /// 分页事件 + /// + public event RoutedEventHandler OnPageIndexChanged; + + /// + /// 分页参数 + /// + private PageArgs pageArgs = new PageArgs(); + + public static readonly DependencyProperty PageIndexProperty = DependencyProperty.Register("PageIndex", typeof(int), typeof(PageControl), new PropertyMetadata(1, new PropertyChangedCallback((s, e) => + { + var pageControl = s as PageControl; + pageControl?.OnIndexChanged(); + }))); + + /// + /// 当前页数 + /// + public int PageIndex + { + get { return (int)GetValue(PageIndexProperty); } + set { SetValue(PageIndexProperty, value); } + } + + public static readonly DependencyProperty PageSizeProperty = DependencyProperty.Register("PageSize", typeof(int), typeof(PageControl), new PropertyMetadata(1)); + + /// + /// 每页记录数 + /// + public int PageSize + { + get { return (int)GetValue(PageSizeProperty); } + set { SetValue(PageSizeProperty, value); } + } + + public static readonly DependencyProperty PageCountProperty = DependencyProperty.Register("PageCount", typeof(int), typeof(PageControl), new PropertyMetadata(1)); + + /// + /// 总页数 + /// + public int PageCount + { + get { return (int)GetValue(PageCountProperty); } + set { SetValue(PageCountProperty, value); } + } + + public static readonly DependencyProperty RecordCountProperty = DependencyProperty.Register("RecordCount", typeof(int), typeof(PageControl), new PropertyMetadata(0, new PropertyChangedCallback((s, e) => + { + var pageControl = s as PageControl; + pageControl?.OnRecordChanged(); + }))); + + /// + /// 总记录数 + /// + public int RecordCount + { + get { return (int)GetValue(RecordCountProperty); } + set { SetValue(RecordCountProperty, value); } + } + + private void OnRecordChanged() + { + PageCount = (RecordCount - 1) / PageSize + 1; + } + + private void OnIndexChanged() + { + pageArgs.PageIndex = this.PageIndex; + OnPageIndexChanged?.Invoke(this, pageArgs); + } + + private void Btn_Click(object sender, RoutedEventArgs e) + { + var btn = sender as BButton; + var newPageIndex = Convert.ToInt32(btn.Content); + if (newPageIndex != PageIndex) + PageIndex = newPageIndex; + else + OnIndexChanged(); + } + + private void btn_first_Click(object sender, RoutedEventArgs e) + { + if (PageIndex > 1) + PageIndex = 1; + } + + private void btn_up_Click(object sender, RoutedEventArgs e) + { + if (PageIndex > 1) + PageIndex--; + } + + private void btn_next_Click(object sender, RoutedEventArgs e) + { + if (PageIndex < PageCount) + PageIndex++; + } + + private void btn_last_Click(object sender, RoutedEventArgs e) + { + if (PageIndex < PageCount) + PageIndex = PageCount; + } + } + + public class PageArgs : RoutedEventArgs + { + public int PageIndex; + //其余自行扩展 + } +} diff --git a/SJ.Controls/RoundWaitProgress.xaml b/SJ.Controls/RoundWaitProgress.xaml new file mode 100644 index 0000000..4789023 --- /dev/null +++ b/SJ.Controls/RoundWaitProgress.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SJ.Controls/RoundWaitProgress.xaml.cs b/SJ.Controls/RoundWaitProgress.xaml.cs new file mode 100644 index 0000000..e26ca31 --- /dev/null +++ b/SJ.Controls/RoundWaitProgress.xaml.cs @@ -0,0 +1,180 @@ +using SJ.Controls.Helpers; +using System; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace SJ.Controls +{ + /// + /// RoundWaitProgress.xaml 的交互逻辑 + /// + public partial class RoundWaitProgress : UserControl + { + public RoundWaitProgress() + { + InitializeComponent(); + this.Loaded += RoundWaitProgress_Loaded; + InitAnimationData(); + } + + private void RoundWaitProgress_Loaded(object sender, RoutedEventArgs e) + { + if (!IsPlaying) + this.Visibility = Visibility.Collapsed; + } + + /// + /// 初始化动画数据 + /// + private void InitAnimationData() + { + if (ControlList == null) + ControlList = new List() { g1, g2, g3, g4, g5 }; + + if (AngleAnimationParamList == null) + AngleAnimationParamList = new List(); + if (BeginOpacityAnimationParamList == null) + BeginOpacityAnimationParamList = new List(); + if (EndOpacityAnimationParamList == null) + EndOpacityAnimationParamList = new List(); + + AngleAnimationParamList.Clear(); + BeginOpacityAnimationParamList.Clear(); + EndOpacityAnimationParamList.Clear(); + + AngleAnimationParamList.Add(new BaseKeyFrame() { Value = 0, _KeyTime = new TimeSpan(0, 0, 0, 0, 0) }); + AngleAnimationParamList.Add(new BaseKeyFrame() { Value = 112, _KeyTime = new TimeSpan(0, 0, 0, 0, 400) }); + AngleAnimationParamList.Add(new BaseKeyFrame() { Value = 202, _KeyTime = new TimeSpan(0, 0, 0, 0, 1500) }); + AngleAnimationParamList.Add(new BaseKeyFrame() { Value = 472, _KeyTime = new TimeSpan(0, 0, 0, 0, 2200) }); + AngleAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 3100), Value = 562 }); + AngleAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 3500), Value = 720 }); + + BeginOpacityAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 0), Value = 0 }); + BeginOpacityAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 1), Value = 1 }); + + EndOpacityAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 3499), Value = 1 }); + EndOpacityAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 3500), Value = 0 }); + EndOpacityAnimationParamList.Add(new BaseKeyFrame() { _KeyTime = new TimeSpan(0, 0, 0, 0, 4500), Value = 0 }); + } + + /// + /// 动画属性 + /// + public static readonly string AnglePropertyPath = "(UIElement.RenderTransform).(RotateTransform.Angle)"; + public static readonly string OpacityPropertyPath = "(UIElement.Opacity)"; + private IList AngleAnimationParamList; + private IList BeginOpacityAnimationParamList; + private IList EndOpacityAnimationParamList; + private IList ControlList; + private Storyboard _Storyboard; + private bool IsPlaying; + + /// + /// 间隔时间 + /// + public static readonly TimeSpan IntervalTimeSpan = new TimeSpan(0, 0, 0, 0, 200); + + public static readonly DependencyProperty WaitTextProperty = DependencyProperty.Register("WaitText", typeof(string), typeof(RoundWaitProgress), new PropertyMetadata("正在加载数据")); + + public string WaitText + { + get { return GetValue(WaitTextProperty).ToString(); } + set { SetValue(WaitTextProperty, value); } + } + + public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Brush), typeof(RoundWaitProgress), new PropertyMetadata(new SolidColorBrush(Colors.Black))); + + public Brush Color + { + get { return GetValue(ColorProperty) as Brush; } + set { SetValue(ColorProperty, value); } + } + + public static readonly DependencyProperty AnimationSizeProperty = DependencyProperty.Register("AnimationSize", typeof(double), typeof(RoundWaitProgress), new PropertyMetadata(80.0)); + + public double AnimationSize + { + get { return Convert.ToDouble(GetValue(AnimationSizeProperty)); } + set { SetValue(AnimationSizeProperty, value); } + } + + public static readonly DependencyProperty PlayProperty = DependencyProperty.Register("Play", typeof(bool), typeof(RoundWaitProgress), new PropertyMetadata(false, (s, e) => + { + var waitControl = s as RoundWaitProgress; + if (waitControl.Play) + waitControl.Start(); + else + waitControl.Stop(); + })); + + public bool Play + { + get { return (bool)GetValue(PlayProperty); } + set { SetValue(PlayProperty, value); } + } + + private void Start() + { + this.IsPlaying = true; + this.Visibility = Visibility.Visible; + this._Storyboard = new Storyboard(); + foreach (var frameElement in ControlList) + { + (frameElement.RenderTransform as RotateTransform).Angle = 0; + DoubleAnimationUsingKeyFrames daukf = new DoubleAnimationUsingKeyFrames(); + foreach (var item in AngleAnimationParamList) + { + daukf.KeyFrames.Add(new EasingDoubleKeyFrame(Convert.ToDouble(item.Value), item._KeyTime)); + } + Storyboard.SetTargetProperty(daukf, new PropertyPath(AnglePropertyPath)); + Storyboard.SetTarget(daukf, frameElement); + this._Storyboard.Children.Add(daukf); + + DoubleAnimationUsingKeyFrames daukf1 = new DoubleAnimationUsingKeyFrames(); + foreach (var item in BeginOpacityAnimationParamList) + { + daukf1.KeyFrames.Add(new EasingDoubleKeyFrame(Convert.ToDouble(item.Value), item._KeyTime)); + } + foreach (var item in EndOpacityAnimationParamList) + { + daukf1.KeyFrames.Add(new EasingDoubleKeyFrame(Convert.ToDouble(item.Value), item._KeyTime)); + } + Storyboard.SetTargetProperty(daukf1, new PropertyPath(OpacityPropertyPath)); + Storyboard.SetTarget(daukf1, frameElement); + this._Storyboard.Children.Add(daukf1); + + for (var i = 0; i < AngleAnimationParamList.Count; i++) + { + var item = AngleAnimationParamList[i]; + item._KeyTime = item._KeyTime.Add(IntervalTimeSpan); + } + + foreach (var item in BeginOpacityAnimationParamList) + { + item._KeyTime = item._KeyTime.Add(IntervalTimeSpan); + } + + foreach (var item in EndOpacityAnimationParamList) + { + item._KeyTime = item._KeyTime.Add(IntervalTimeSpan); + } + + this._Storyboard.RepeatBehavior = RepeatBehavior.Forever; + } + this._Storyboard.Begin(); + } + + private void Stop() + { + this._Storyboard.Stop(); + this._Storyboard.Children.Clear(); + this._Storyboard = null; + this.IsPlaying = false; + InitAnimationData(); + this.Visibility = Visibility.Collapsed; + } + } +} diff --git a/SJ.Controls/SJ.Controls.csproj b/SJ.Controls/SJ.Controls.csproj new file mode 100644 index 0000000..94b56da --- /dev/null +++ b/SJ.Controls/SJ.Controls.csproj @@ -0,0 +1,17 @@ + + + + net6.0-windows + enable + true + + + + + + + + + + + diff --git a/SJ.Controls/Themes/Generic.xaml b/SJ.Controls/Themes/Generic.xaml new file mode 100644 index 0000000..94b2b51 --- /dev/null +++ b/SJ.Controls/Themes/Generic.xaml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + +