diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b31d69a..e8dcbef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -简体中文 | [English](./CONTRIBUTING_en.md) +简体中文 | [繁體中文](./CONTRIBUTING_zht.md) | [English](./CONTRIBUTING_en.md) ## 参与开发 diff --git a/CONTRIBUTING_en.md b/CONTRIBUTING_en.md index 820a9b4..996ee24 100644 --- a/CONTRIBUTING_en.md +++ b/CONTRIBUTING_en.md @@ -1,6 +1,6 @@ # Contributing -[简体中文](./CONTRIBUTING.md) | English +[简体中文](./CONTRIBUTING.md) | [繁體中文](./CONTRIBUTING_zht.md) | English Welcome to participate in development! To ensure code maintainability and playability, if you wish to develop new features or gameplay, please first contact me (by sending a [mail](mailto:zoujin.dev@exlb.org) or opening an [Issue](https://github.com/LorisYounger/VPet/issues/new)) with your idea. This is to make sure your contribution fits the game, and will not get outright rejected for being unfitting (causing your effort to be wasted). You don't need to contact me regarding fixing errors or bugs - simply send a PR in that case. @@ -88,4 +88,4 @@ You **must** inform users of the source of our animation files, and provide a li * All of the above authorization information **must** be disclosed. * You **must** provide a link to [this page](https://github.com/LorisYounger/VPet). -* **No** profit should be made with our files. \ No newline at end of file +* **No** profit should be made with our files. diff --git a/CONTRIBUTING_zht.md b/CONTRIBUTING_zht.md new file mode 100644 index 0000000..7f0db07 --- /dev/null +++ b/CONTRIBUTING_zht.md @@ -0,0 +1,89 @@ +[简体中文](./CONTRIBUTING.md) | 繁體中文 | [English](./CONTRIBUTING_en.md) + +## 參與開發 + +歡迎參與虛擬桌寵模擬器的開發!為了保證程式碼的可維護性及遊戲性,若想要開發新的功能,請先[電子郵件聯絡](mailto:zoujin.dev@exlb.org)或提交[Issue](https://github.com/LorisYounger/VPet/issues),標題為想要新增的功能/玩法,以確保該功能/玩法適用於虛擬桌寵模擬器,以免在您完成開發後,因不適合而被拒絕(而浪費您的時間)。
+如果是修正錯誤或BUG,則不需要先行聯絡,修好後直接提交即可。 + +當您提供的想法被贊同後,您可以使用[Fork](https://github.com/LorisYounger/VPet/fork)功能,將專案程式碼整個複製至個人的Github上,以便撰寫自己的程式碼。撰寫完畢後,使用[Pull Requests](https://github.com/LorisYounger/VPet/compare)提交。
+若您的想法並未被同意,也可以另起爐灶,開發一個不同版本及功能的桌寵軟體。須遵守[Apache License 2.0](https://github.com/LorisYounger/VPet/blob/main/LICENSE)及[動畫版權聲明與授權](https://github.com/LorisYounger/VPet/blob/main/README_zht.md#%E5%8B%95%E7%95%AB%E7%89%88%E6%AC%8A%E8%81%B2%E6%98%8E%E8%88%87%E6%8E%88%E6%AC%8A)。
+註:一般而言,加入新功能都可以透過撰寫模組來達成,詳情請見:[VPet.Plugin.Demo](https://github.com/LorisYounger/VPet.Plugin.Demo) + +作者可能會修改、刪減部分您所提交的程式碼,以確保該功能/玩法適用於虛擬桌寵模擬器。 + +## 動畫版權聲明與授權 + +在Github中,[桌寵動畫檔案](https://github.com/LorisYounger/VPet/tree/main/VPet-Simulator.Windows/mod/0000_core/pet/vup)之動畫版權歸[虛擬主播模擬器製作組](https://www.exlb.net/VUP-Simulator)所有,在使用本類別庫時,您可能會需要自行準備動畫檔,或遵循下列協定: + +### 非商業用途授權 + +* 需要向使用者告知動畫檔案的來源,並提供造訪[本頁面](https://github.com/LorisYounger/VPet)的連結 +* 當您完成上述要求後,可以免費使用動畫檔案 + +### 商業用途授權(低於10萬) + +* 在使用者第一次使用時,需跳出視窗,並醒目向使用者告知動畫檔案的來源,並提供造訪[本頁面](https://github.com/LorisYounger/VPet)的連結 +* 在對應的頁面上(使用者能快速造訪的),向使用者告知動畫檔案的來源,並提供造訪[本頁面](https://github.com/LorisYounger/VPet)的連結 +* 當您完成上述要求後,可以免費使用動畫檔案 + +### 商業用途授權(高於10萬或其他) + +* 請[電子郵件聯絡](mailto:zoujin.dev@exlb.org)本軟體作者 + +### 轉發動畫檔案 + +* 需要告知上述所有授權資訊 +* 需要提供造訪[本頁面](https://github.com/LorisYounger/VPet)的連結 +* 轉發動畫檔案時,禁止任何付費或收費行為 + +## 桌面應用程式部署方式 + +1. 下載本專案,透過VisualStudio開啟`VPet.sln`檔案 +2. 在「建置」選項中,選擇位元數`x64`及建置專案`Vpet-Simulator.Windows` + ![image-20230208004330895](README.assets/image-20230208004330895.png) +3. 點擊「開始」,若一切順利將會報錯`缺少Core模組,無法啟動桌寵` +4. 以管理員身分執行`mklink.bat`,這會讓模組檔案連結至產生的位置 +5. 再次點擊啟動即可正常執行 + +## 軟體架構 + +* **VPet-Simulator.Windows: 適用於桌面端的虛擬桌寵模擬器** + * *Function 功能性程式碼儲存位置* + * CoreMOD 模組管理 + * MWController 視窗控制器 + + * *WinDesign 視窗及UI設計 + * winBetterBuy 更好買視窗 + * winCGPTSetting ChatGPT設定 + * winSetting 軟體設定、模組視窗 + * winConsole 開發控制台 + * winGameSetting 遊戲設定 + * winReport 意見回饋中心 + + * MainWindows 主視窗、儲存及展示Core + * PetHelper 快速切換圖示 +* **VPet-Simulator.Tool: 方便製作模組的工具(例如:產生動態圖片)** +* **VPet-Simulator.Core: 軟體核心,方便內建至任何的WPF應用程式(例如:VUP-Simulator)** + * Handle 介面及控制項 + * IController 視窗控制(呼叫相關功能及設定,例如:移動到側邊等) + * Function 通用功能 + * GameCore 遊戲核心,包含各種資料數據等內容 + * GameSave 遊戲存檔 + * IFood 食物及物品介面 + * PetLoader 寵物圖片載入器 + * Graph 圖形渲染 + * IGraph 動畫基本介面 + * GraphCore 動畫顯示核心 + * GraphHelper 動畫幫助 + * GraphInfo 動畫資訊 + * FoodAnimation 食物動畫,支援顯示前中後三層夾心動畫,不一定只用於食物,只是叫這個名字 + * PNGAnimation 桌寵動態動畫元件 + * Picture 桌寵靜態動畫元件 + * Display 顯示 + * basestyle/Theme 基礎風格主題 + * Main.xaml 核心顯示元件 + * MainDisplay 核心顯示方法 + * MainLogic 核心顯示邏輯 + * ToolBar 點擊人物時的工具欄 + * MessageBar 人物說話時的對話框 + * WorkTimer 運作計時器 diff --git a/VPet-Simulator.Windows/Function/CoreMOD.cs b/VPet-Simulator.Windows/Function/CoreMOD.cs index 180b0a7..6fb61ad 100644 --- a/VPet-Simulator.Windows/Function/CoreMOD.cs +++ b/VPet-Simulator.Windows/Function/CoreMOD.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Windows; +using System.Xml.Linq; using VPet_Simulator.Core; using VPet_Simulator.Windows.Interface; @@ -104,6 +105,13 @@ namespace VPet_Simulator.Windows LocalizeCore.AddCulture(line.info, ls); } + if (mw.CoreMODs.FirstOrDefault(x => x.Name == Name) != null) + { + Name += $"({"MOD名称重复".Translate()})"; + ErrorMessage = "MOD名称重复".Translate(); + return; + } + if (!IsOnMOD(mw)) { Tag.Add("该模组已停用"); @@ -155,6 +163,8 @@ namespace VPet_Simulator.Windows var tmp = new LpsDocument(File.ReadAllText(fi.FullName)); foreach (ILine li in tmp) { + if (li.Name != "food") + continue; string tmps = li.Find("name").info; mw.Foods.RemoveAll(x => x.Name == tmps); mw.Foods.Add(LPSConvert.DeserializeObject(li)); diff --git a/VPet-Simulator.Windows/MainWindow.cs b/VPet-Simulator.Windows/MainWindow.cs index 8ba2107..37e48fc 100644 --- a/VPet-Simulator.Windows/MainWindow.cs +++ b/VPet-Simulator.Windows/MainWindow.cs @@ -1205,6 +1205,7 @@ namespace VPet_Simulator.Windows public async void GameLoad(List Path) { + Path = Path.Distinct().ToList(); await Dispatcher.InvokeAsync(new Action(() => LoadingText.Content = "Loading MOD")); //加载mod foreach (DirectoryInfo di in Path) diff --git a/VPet-Simulator.Windows/WinDesign/winBetterBuy.xaml.cs b/VPet-Simulator.Windows/WinDesign/winBetterBuy.xaml.cs index 79f0a66..23846d5 100644 --- a/VPet-Simulator.Windows/WinDesign/winBetterBuy.xaml.cs +++ b/VPet-Simulator.Windows/WinDesign/winBetterBuy.xaml.cs @@ -353,6 +353,8 @@ namespace VPet_Simulator.Windows private void pagination_CurrentPageChanged(object sender, SelectedValueChangedRoutedEventArgs e) { + if (!AllowChange) + return; Search(); TbPage.Text = e.NewValue.ToString(); } diff --git a/VPet-Simulator.Windows/mod/0000_core/food/timelimit.lps b/VPet-Simulator.Windows/mod/0000_core/food/timelimit.lps index af1dd71..903e4b7 100644 --- a/VPet-Simulator.Windows/mod/0000_core/food/timelimit.lps +++ b/VPet-Simulator.Windows/mod/0000_core/food/timelimit.lps @@ -1,3 +1,9 @@ -food:|name#莲蓉蛋黄月饼:|type#Snack:|Exp#100:|Strength#40:|StrengthDrink#-3:|StrengthFood#80:|Likability#1:|Health#5:|Feeling#40:|price#26:|graph#eat:|desc#海上生明月,天涯共此时。无论近况如何,前途多难,计划如不如意,未来迷不迷茫,萝莉丝都为你做了莲蓉蛋黄月饼,快说谢谢萝莉丝。:| -food:|name#五仁月饼:|type#Meal:|Exp#100:|Strength#80:|StrengthDrink#-6:|StrengthFood#160:|Likability#1:|Health#5:|Feeling#0:|price#38:|graph#eat:|desc#举头望明月,低头思故乡。无论身在何处,离家多远,家人在不在身边,心情好不好,萝莉丝都为你做了五仁月饼,快说谢谢萝莉丝。:| -food:|name#巧克力花生月饼:|type#Functional:|Exp#400:|Strength#20:|StrengthDrink#0:|StrengthFood#40:|Likability#2:|Health#5:|Feeling#20:|price#62:|graph#eat:|desc#明月几时有,把酒问青天。无论考的如何,作业有多少,老师怎么说,家长怎么批评,萝莉丝都为你做了巧克力花生月饼,快说谢谢萝莉丝。:| \ No newline at end of file +/// food:|name#莲蓉蛋黄月饼:|type#Snack:|Exp#100:|Strength#40:|StrengthDrink#-3:|StrengthFood#80:|Likability#1:|Health#5:|Feeling#40:|price#26:|graph#eat:|desc#海上生明月,天涯共此时。无论近况如何,前途多难,计划如不如意,未来迷不迷茫,萝莉丝都为你做了莲蓉蛋黄月饼,快说谢谢萝莉丝。:| +/// food:|name#五仁月饼:|type#Meal:|Exp#100:|Strength#80:|StrengthDrink#-6:|StrengthFood#160:|Likability#1:|Health#5:|Feeling#0:|price#38:|graph#eat:|desc#举头望明月,低头思故乡。无论身在何处,离家多远,家人在不在身边,心情好不好,萝莉丝都为你做了五仁月饼,快说谢谢萝莉丝。:| +/// food:|name#巧克力花生月饼:|type#Functional:|Exp#400:|Strength#20:|StrengthDrink#0:|StrengthFood#40:|Likability#2:|Health#5:|Feeling#20:|price#62:|graph#eat:|desc#明月几时有,把酒问青天。无论考的如何,作业有多少,老师怎么说,家长怎么批评,萝莉丝都为你做了巧克力花生月饼,快说谢谢萝莉丝。:| + +food:|name#圣诞帽:|type#Gift:|Exp#200:|Likability#4:|Health#5:|Feeling#50:|price#40.0:|graph#gift:|desc#圣诞帽_giftintor:| +food:|name#礼物盒子:|type#Gift:|Exp#1000:|Likability#5:|Feeling#100:|price#150.0:|graph#gift:|desc#礼物盒子_giftintor:| +food:|name#糖果棒:|type#Snack:|Exp#200:|Strength#40:|StrengthFood#40:|Likability#2:|Feeling#40:|price#35:|graph#eat:|desc#糖果棒_giftintor:| +food:|name#圣诞草莓奶茶:|type#Drink:|Exp#200:|StrengthDrink#80:|Likability#2:|Feeling#60:|price#35:|graph#drink:|desc#圣诞草莓奶茶_giftintor:| +food:|name#萝莉丝姜饼人:|type#Snack:|Exp#600:|StrengthFood#80:|Likability#2:|Health#2:|Feeling#50:|price#128.0:|graph#eat:|desc#萝莉丝姜饼人_giftintor:| diff --git a/VPet-Simulator.Windows/mod/0000_core/image/food/圣诞帽.png b/VPet-Simulator.Windows/mod/0000_core/image/food/圣诞帽.png new file mode 100644 index 0000000..c394dab Binary files /dev/null and b/VPet-Simulator.Windows/mod/0000_core/image/food/圣诞帽.png differ diff --git a/VPet-Simulator.Windows/mod/0000_core/image/food/圣诞草莓奶茶.png b/VPet-Simulator.Windows/mod/0000_core/image/food/圣诞草莓奶茶.png new file mode 100644 index 0000000..8fa9e4b Binary files /dev/null and b/VPet-Simulator.Windows/mod/0000_core/image/food/圣诞草莓奶茶.png differ diff --git a/VPet-Simulator.Windows/mod/0000_core/image/food/礼物盒子.png b/VPet-Simulator.Windows/mod/0000_core/image/food/礼物盒子.png new file mode 100644 index 0000000..365ae64 Binary files /dev/null and b/VPet-Simulator.Windows/mod/0000_core/image/food/礼物盒子.png differ diff --git a/VPet-Simulator.Windows/mod/0000_core/image/food/糖果棒.png b/VPet-Simulator.Windows/mod/0000_core/image/food/糖果棒.png new file mode 100644 index 0000000..309e3ae Binary files /dev/null and b/VPet-Simulator.Windows/mod/0000_core/image/food/糖果棒.png differ diff --git a/VPet-Simulator.Windows/mod/0000_core/image/food/萝莉丝姜饼人.png b/VPet-Simulator.Windows/mod/0000_core/image/food/萝莉丝姜饼人.png new file mode 100644 index 0000000..b755557 Binary files /dev/null and b/VPet-Simulator.Windows/mod/0000_core/image/food/萝莉丝姜饼人.png differ diff --git a/VPet-Simulator.Windows/mod/0000_core/lang/en/Limit.lps b/VPet-Simulator.Windows/mod/0000_core/lang/en/Limit.lps index 835706f..9449cdf 100644 --- a/VPet-Simulator.Windows/mod/0000_core/lang/en/Limit.lps +++ b/VPet-Simulator.Windows/mod/0000_core/lang/en/Limit.lps @@ -7,4 +7,14 @@ 主人,sbeam秋季促销开始了哦,还有游戏大奖赛,快去给{name}去投一票吧。#Master, Sbeam's autumn promotion has begun, and there is also a Awards . Go and vote for {name}.:| 主人主人,{name}参加了sbeam大奖赛哦,给人家投一票喵#Master, {name} is participating in the Sbeam Awards , vote for me meow:| 那个。。主人。。\n人家参加了sbeam大奖赛哦。能不能。。给{name}投一票呢~#That... Master... I'm participating in the Sbeam Awards . Can you... vote for {name}~:| -电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧#There is a game called "VPet Simulator" in the computer that is participating in the 2023 Sbeam Awards , come and vote for the desktop pet:| \ No newline at end of file +电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧#There is a game called "VPet Simulator" in the computer that is participating in the 2023 Sbeam Awards , come and vote for the desktop pet:| +圣诞帽#Christmas hat:| +圣诞帽_giftintor#An ordinary hat, but anyone can be Santa with it.:| +礼物盒子#Gift box:| +礼物盒子_giftintor#Surprise!!:| +糖果棒#Candy bar:| +糖果棒_giftintor#You can eat it, play with it, and use it to spank bad kids.:| +圣诞草莓奶茶#Christmas strawberry milk tea:| +圣诞草莓奶茶_giftintor#I don't know why it's Christmas cream tea and not Christmas cake, maybe the artist likes Christmas strawberry cream tea:| +萝莉丝姜饼人#Gingerbread Lolis:| +萝莉丝姜饼人_giftintor#Lolis made her own gingerbread man, and Lolis would get mad if she couldn't eat it because it was too cute!:| \ No newline at end of file diff --git a/VPet-Simulator.Windows/mod/0000_core/lang/en/Prog2312.lps b/VPet-Simulator.Windows/mod/0000_core/lang/en/Prog2312.lps new file mode 100644 index 0000000..ace77c6 --- /dev/null +++ b/VPet-Simulator.Windows/mod/0000_core/lang/en/Prog2312.lps @@ -0,0 +1 @@ +MOD名称重复#MOD name duplicated:| \ No newline at end of file diff --git a/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Limit.lps b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Limit.lps index 5d01424..04dae7b 100644 --- a/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Limit.lps +++ b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Limit.lps @@ -4,7 +4,17 @@ 海上生明月,天涯共此时。无论近况如何,前途多难,计划如不如意,未来迷不迷茫,萝莉丝都为你做了莲蓉蛋黄月饼,快说谢谢萝莉丝。#海上生明月,天涯共此时。无论近况如何,前途多难,计划如不如意,未来迷不迷茫,萝莉丝都为你做了莲蓉蛋黄月饼,快说谢谢萝莉丝。:| 巧克力花生月饼#巧克力花生月饼:| 明月几时有,把酒问青天。无论考的如何,作业有多少,老师怎么说,家长怎么批评,萝莉丝都为你做了巧克力花生月饼,快说谢谢萝莉丝。#明月几时有,把酒问青天。无论考的如何,作业有多少,老师怎么说,家长怎么批评,萝莉丝都为你做了巧克力花生月饼,快说谢谢萝莉丝。:| -主人,sbema秋季促销开始了哦,还有游戏大奖赛,快去给{name}去投一票吧。 -主人主人,{name}参加了sbeam大奖赛哦,给人家投一票喵 -那个。。主人。。\n人家参加了sbeam大奖赛哦。能不能。。给{name}投一票呢~ -电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧 \ No newline at end of file +主人,sbema秋季促销开始了哦,还有游戏大奖赛,快去给{name}去投一票吧。#主人,sbema秋季促销开始了哦,还有游戏大奖赛,快去给{name}去投一票吧。:| +主人主人,{name}参加了sbeam大奖赛哦,给人家投一票喵#主人主人,{name}参加了sbeam大奖赛哦,给人家投一票喵:| +那个。。主人。。\n人家参加了sbeam大奖赛哦。能不能。。给{name}投一票呢~#那个。。主人。。\n人家参加了sbeam大奖赛哦。能不能。。给{name}投一票呢~:| +电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧#电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧:| +圣诞帽#圣诞帽:| +圣诞帽_giftintor#一顶普通的帽子,但带上它谁都可以成为圣诞老人:| +礼物盒子#礼物盒子:| +礼物盒子_giftintor#Surprise!!:| +糖果棒#糖果棒:| +糖果棒_giftintor#能吃能玩,还能用来打坏小孩的屁股:| +圣诞草莓奶茶#圣诞草莓奶茶:| +圣诞草莓奶茶_giftintor#我不知道为什么是圣诞奶茶而不是圣诞蛋糕,可能画师喜欢圣诞草莓奶茶吧:| +萝莉丝姜饼人#萝莉丝姜饼人:| +萝莉丝姜饼人_giftintor#萝莉丝做的自己样子的姜饼人,要是因为太可爱舍不得吃萝莉丝会生气的哦:| \ No newline at end of file diff --git a/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Prog2312.lps b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Prog2312.lps new file mode 100644 index 0000000..4f7d8de --- /dev/null +++ b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hans/Prog2312.lps @@ -0,0 +1 @@ +MOD名称重复#MOD名称重复:| \ No newline at end of file diff --git a/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Limit.lps b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Limit.lps index b1ec887..e2cc654 100644 --- a/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Limit.lps +++ b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Limit.lps @@ -7,4 +7,14 @@ 主人,sbeam秋季促销开始了哦,还有游戏大奖赛,快去给{name}去投一票吧。#主人,sbeam秋季促銷開始了哦,還有遊戲大獎賽,快去給{name}去投一票吧。:| 主人主人,{name}参加了sbeam大奖赛哦,给人家投一票喵#主人主人,{name}參加了sbeam大獎賽哦,給人家投一票喵:| 那个。。主人。。\n人家参加了sbeam大奖赛哦。能不能。。给{name}投一票呢~#那個。。主人。。\n人家參加了sbeam大獎賽哦。能不能。。給{name}投一票呢~:| -电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧#電腦裡有一款《虛擬桌寵模擬器》的遊戲正在參加2023的sbeam大獎賽,快來給桌寵投一票吧:| \ No newline at end of file +电脑里有一款《虚拟桌宠模拟器》的游戏正在参加2023的sbeam大奖赛,快来给桌宠投一票吧#電腦裡有一款《虛擬桌寵模擬器》的遊戲正在參加2023的sbeam大獎賽,快來給桌寵投一票吧:| +圣诞帽#聖誕帽:| +圣诞帽_giftintor#一頂普通的帽子,但帶上它誰都可以成為聖誕老人:| +礼物盒子#禮物盒子:| +礼物盒子_giftintor#Surprise!!:| +糖果棒#糖果棒:| +糖果棒_giftintor#能吃能玩,還能用來打壞小孩的屁股:| +圣诞草莓奶茶#聖誕草莓奶茶:| +圣诞草莓奶茶_giftintor#我不知道為什麼是聖誕奶茶而不是聖誕蛋糕,可能畫師喜歡聖誕草莓奶茶吧:| +萝莉丝姜饼人#蘿莉斯薑餅人:| +萝莉丝姜饼人_giftintor#蘿莉絲做的自己樣子的薑餅人,要是因為太可愛捨不得吃蘿莉絲會生氣的哦:| diff --git a/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Prog2312.lps b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Prog2312.lps new file mode 100644 index 0000000..cbf4124 --- /dev/null +++ b/VPet-Simulator.Windows/mod/0000_core/lang/zh-Hant/Prog2312.lps @@ -0,0 +1 @@ +MOD名称重复#MOD名稱重複:| \ No newline at end of file diff --git a/VPet.Solution/App.xaml b/VPet.Solution/App.xaml index 085dea1..f8cd87d 100644 --- a/VPet.Solution/App.xaml +++ b/VPet.Solution/App.xaml @@ -1,9 +1,17 @@ - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/VPet.Solution/App.xaml.cs b/VPet.Solution/App.xaml.cs index a7005a0..bde1717 100644 --- a/VPet.Solution/App.xaml.cs +++ b/VPet.Solution/App.xaml.cs @@ -1,42 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; +using System.Diagnostics; using System.Windows; -using VPet_Simulator.Windows.Interface; -namespace VPet.Solution +namespace VPet.Solution; + +/// +/// App.xaml 的交互逻辑 +/// +public partial class App : Application { - /// - /// App.xaml 的交互逻辑 - /// - public partial class App : Application + public static bool IsDone { get; set; } = false; + + protected override void OnStartup(StartupEventArgs e) { - public static bool IsDone { get; set; } = false; - protected override void OnStartup(StartupEventArgs e) + if (e.Args != null && e.Args.Count() > 0) { - if (e.Args != null && e.Args.Count() > 0) + IsDone = true; + switch (e.Args[0].ToLower()) { - IsDone = true; - switch (e.Args[0].ToLower()) - { - case "removestarup": - var path = Environment.GetFolderPath(Environment.SpecialFolder.Startup) + @"\VPET_Simulator.lnk"; - if (File.Exists(path)) - { - File.Delete(path); - } - return; - case "launchsteam": - Process.Start("steam://rungameid/1920960"); - return; - } + case "removestarup": + var path = + Environment.GetFolderPath(Environment.SpecialFolder.Startup) + + @"\VPET_Simulator.lnk"; + if (File.Exists(path)) + { + File.Delete(path); + } + return; + case "launchsteam": + Process.Start("steam://rungameid/1920960"); + return; } - IsDone = false; } + IsDone = false; } } diff --git a/VPet.Solution/Converters.xaml b/VPet.Solution/Converters.xaml new file mode 100644 index 0000000..45ff5da --- /dev/null +++ b/VPet.Solution/Converters.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/VPet.Solution/Converters/BoolInverter.cs b/VPet.Solution/Converters/BoolInverter.cs new file mode 100644 index 0000000..d8a5e55 --- /dev/null +++ b/VPet.Solution/Converters/BoolInverter.cs @@ -0,0 +1,19 @@ +using System.Globalization; +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class BoolInverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not bool b) + return false; + return !b; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/BrushToMediaColorConverter.cs b/VPet.Solution/Converters/BrushToMediaColorConverter.cs new file mode 100644 index 0000000..773e095 --- /dev/null +++ b/VPet.Solution/Converters/BrushToMediaColorConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace VPet.House.Converters; + +public class BrushToMediaColorConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not SolidColorBrush brush) + throw new ArgumentException("Not SolidColorBrush", nameof(value)); + return brush.Color; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not Color color) + throw new ArgumentException("Not media color", nameof(value)); + return new SolidColorBrush(color); + } +} diff --git a/VPet.Solution/Converters/CalculatorConverter.cs b/VPet.Solution/Converters/CalculatorConverter.cs new file mode 100644 index 0000000..0bfab22 --- /dev/null +++ b/VPet.Solution/Converters/CalculatorConverter.cs @@ -0,0 +1,97 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace VPet.House.Converters; + +/// +/// 计算器转换器 +/// 示例: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// // +/// +/// +/// +/// +/// +/// +/// +/// ]]> +/// +/// 绑定的数量不正确 +public class CalculatorConverter : IMultiValueConverter +{ + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Any(i => i == DependencyProperty.UnsetValue)) + return 0.0; + if (values.Length == 1) + return values[0]; + double result = System.Convert.ToDouble(values[0]); + if (parameter is string operators) + { + if (operators.Length != values.Length - 1) + throw new Exception("Parameter error: operator must be one more than parameter"); + for (int i = 1; i < values.Length - 1; i++) + result = Operation(result, operators[i - 1], System.Convert.ToDouble(values[i])); + result = Operation(result, operators.Last(), System.Convert.ToDouble(values.Last())); + } + else + { + if (System.Convert.ToBoolean(values.Length & 1) is false) + throw new Exception("Parameter error: Incorrect quantity"); + bool isNumber = false; + char currentOperator = '0'; + for (int i = 1; i < values.Length - 1; i++) + { + if (isNumber is false) + { + currentOperator = ((string)values[i])[0]; + isNumber = true; + } + else + { + var value = System.Convert.ToDouble(values[i]); + result = Operation(result, currentOperator, value); + isNumber = false; + } + } + result = Operation(result, currentOperator, System.Convert.ToDouble(values.Last())); + } + return result; + } + + public static double Operation(double value1, char operatorChar, double value2) + { + return operatorChar switch + { + '+' => value1 + value2, + '-' => value1 - value2, + '*' => value1 * value2, + '/' => value1 / value2, + '%' => value1 % value2, + _ => throw new NotImplementedException(), + }; + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/EqualsConverter.cs b/VPet.Solution/Converters/EqualsConverter.cs new file mode 100644 index 0000000..84c7a92 --- /dev/null +++ b/VPet.Solution/Converters/EqualsConverter.cs @@ -0,0 +1,28 @@ +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class EqualsConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + System.Globalization.CultureInfo culture + ) + { + if (values.Length != 2) + throw new NotImplementedException("Values length must be 2"); + return values[0].Equals(values[1]); + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + System.Globalization.CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/FalseToCollapsedConverter.cs b/VPet.Solution/Converters/FalseToCollapsedConverter.cs new file mode 100644 index 0000000..9fbe94b --- /dev/null +++ b/VPet.Solution/Converters/FalseToCollapsedConverter.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class FalseToCollapsedConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool.TryParse(value.ToString(), out var result) && result) + ? Visibility.Visible + : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is Visibility visibility && visibility == Visibility.Collapsed; + } +} diff --git a/VPet.Solution/Converters/FalseToHiddenConverter.cs b/VPet.Solution/Converters/FalseToHiddenConverter.cs new file mode 100644 index 0000000..b0856df --- /dev/null +++ b/VPet.Solution/Converters/FalseToHiddenConverter.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class FalseToHiddenConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool.TryParse(value.ToString(), out var result) && result) + ? Visibility.Visible + : Visibility.Hidden; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is Visibility visibility && visibility == Visibility.Hidden; + } +} diff --git a/VPet.Solution/Converters/MarginConverter.cs b/VPet.Solution/Converters/MarginConverter.cs new file mode 100644 index 0000000..2575c79 --- /dev/null +++ b/VPet.Solution/Converters/MarginConverter.cs @@ -0,0 +1,84 @@ +using System.Windows; +using System.Windows.Data; + +namespace VPet.House.Converters; + +/// +/// 边距转换器 +/// 示例: +/// +/// +/// +/// +/// +/// +/// ]]> +/// +public class MarginConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + System.Globalization.CultureInfo culture + ) + { + if (values.Length == 0) + { + return new Thickness(); + } + else if (values.Length == 1) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[0]), + Top = default, + Right = default, + Bottom = default + }; + } + else if (values.Length == 2) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[0]), + Top = System.Convert.ToDouble(values[1]), + Right = default, + Bottom = default + }; + } + else if (values.Length == 3) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[0]), + Top = System.Convert.ToDouble(values[1]), + Right = System.Convert.ToDouble(values[2]), + Bottom = default + }; + } + else if (values.Length == 4) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[0]), + Top = System.Convert.ToDouble(values[1]), + Right = System.Convert.ToDouble(values[2]), + Bottom = System.Convert.ToDouble(values[3]) + }; + } + else + throw new NotImplementedException(); + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + System.Globalization.CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/MaxConverter.cs b/VPet.Solution/Converters/MaxConverter.cs new file mode 100644 index 0000000..3880126 --- /dev/null +++ b/VPet.Solution/Converters/MaxConverter.cs @@ -0,0 +1,26 @@ +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class MaxConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + System.Globalization.CultureInfo culture + ) + { + return values.Max(i => (double)i); + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + System.Globalization.CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/MediaColorToBrushConverter.cs b/VPet.Solution/Converters/MediaColorToBrushConverter.cs new file mode 100644 index 0000000..e513ca1 --- /dev/null +++ b/VPet.Solution/Converters/MediaColorToBrushConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace VPet.House.Converters; + +public class MediaColorToBrushConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not Color color) + throw new ArgumentException("Not media color", nameof(value)); + return new SolidColorBrush(color); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not SolidColorBrush brush) + throw new ArgumentException("Not SolidColorBrush", nameof(value)); + return brush.Color; + } +} diff --git a/VPet.Solution/Converters/NotEqualsConverter.cs b/VPet.Solution/Converters/NotEqualsConverter.cs new file mode 100644 index 0000000..0559d8f --- /dev/null +++ b/VPet.Solution/Converters/NotEqualsConverter.cs @@ -0,0 +1,28 @@ +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class NotEqualsConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + System.Globalization.CultureInfo culture + ) + { + if (values.Length != 2) + throw new NotImplementedException("Values length must be 2"); + return !values[0].Equals(values[1]); + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + System.Globalization.CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/NullToFalseConverter.cs b/VPet.Solution/Converters/NullToFalseConverter.cs new file mode 100644 index 0000000..909721c --- /dev/null +++ b/VPet.Solution/Converters/NullToFalseConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using System.Windows.Data; + +namespace VPet.House.Converters; + +public class NullToFalseConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is not null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/RatioMarginConverter.cs b/VPet.Solution/Converters/RatioMarginConverter.cs new file mode 100644 index 0000000..5e31dac --- /dev/null +++ b/VPet.Solution/Converters/RatioMarginConverter.cs @@ -0,0 +1,91 @@ +using System.Windows; +using System.Windows.Data; + +namespace VPet.House.Converters; + +/// +/// 边距转换器 +/// 示例: +/// +/// +/// +/// +/// +/// +/// ]]> +/// +public class RatioMarginConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + System.Globalization.CultureInfo culture + ) + { + if (values.Any(i => i == DependencyProperty.UnsetValue)) + return new Thickness(); + if (values.Length == 0) + { + return new Thickness(); + } + else if (values.Length == 1) + { + return new Thickness(); + } + var ratio = System.Convert.ToDouble(values[0]); + if (values.Length == 2) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[1]) * ratio, + Top = default, + Right = default, + Bottom = default + }; + } + else if (values.Length == 3) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[1]) * ratio, + Top = System.Convert.ToDouble(values[2]) * ratio, + Right = default, + Bottom = default + }; + } + else if (values.Length == 4) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[1]) * ratio, + Top = System.Convert.ToDouble(values[2]) * ratio, + Right = System.Convert.ToDouble(values[3]) * ratio, + Bottom = default + }; + } + else if (values.Length == 5) + { + return new Thickness() + { + Left = System.Convert.ToDouble(values[1]) * ratio, + Top = System.Convert.ToDouble(values[2]) * ratio, + Right = System.Convert.ToDouble(values[3]) * ratio, + Bottom = System.Convert.ToDouble(values[4]) * ratio + }; + } + else + throw new NotImplementedException(); + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + System.Globalization.CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/Converters/StringFormatConverter.cs b/VPet.Solution/Converters/StringFormatConverter.cs new file mode 100644 index 0000000..26730e0 --- /dev/null +++ b/VPet.Solution/Converters/StringFormatConverter.cs @@ -0,0 +1,51 @@ +using System.Windows.Data; + +namespace VPet.House.Converters; + +/// +/// 边距转换器 +/// 示例: +/// +/// +/// +/// +/// +/// OR +/// +/// +/// +/// +/// ]]> +/// +public class StringFormatConverter : IMultiValueConverter +{ + public object Convert( + object[] values, + Type targetType, + object parameter, + System.Globalization.CultureInfo culture + ) + { + var formatStr = (string)parameter; + if (string.IsNullOrWhiteSpace(formatStr)) + { + formatStr = (string)values[0]; + return string.Format(formatStr, values.Skip(1).ToArray()); + } + else + { + return string.Format(formatStr, values); + } + } + + public object[] ConvertBack( + object value, + Type[] targetTypes, + object parameter, + System.Globalization.CultureInfo culture + ) + { + throw new NotImplementedException(); + } +} diff --git a/VPet.Solution/MainWindow.xaml b/VPet.Solution/MainWindow.xaml deleted file mode 100644 index d01cfd8..0000000 --- a/VPet.Solution/MainWindow.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/VPet.Solution/NativeStyles.xaml b/VPet.Solution/NativeStyles.xaml new file mode 100644 index 0000000..4710477 --- /dev/null +++ b/VPet.Solution/NativeStyles.xaml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/VPet.Solution/NativeStyles.xaml.cs b/VPet.Solution/NativeStyles.xaml.cs new file mode 100644 index 0000000..ed6f24c --- /dev/null +++ b/VPet.Solution/NativeStyles.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace VPet.Solution; + +public partial class NativeStyles : ResourceDictionary +{ + public NativeStyles() + { + InitializeComponent(); + } +} diff --git a/VPet.Solution/Properties/AssemblyInfo.cs b/VPet.Solution/Properties/AssemblyInfo.cs index c186668..dfb6a99 100644 --- a/VPet.Solution/Properties/AssemblyInfo.cs +++ b/VPet.Solution/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; @@ -33,15 +31,14 @@ using System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, //主题特定资源词典所处位置 - //(未在页面中找到资源时使用, - //或应用程序资源字典中找到时使用) + //(未在页面中找到资源时使用, + //或应用程序资源字典中找到时使用) ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置 - //(未在页面中找到资源时使用, - //、应用程序或任何主题专用资源字典中找到时使用) +//(未在页面中找到资源时使用, +//、应用程序或任何主题专用资源字典中找到时使用) )] - -// 程序集的版本信息由下列四个值组成: +// 程序集的版本信息由下列四个值组成: // // 主版本 // 次版本 diff --git a/VPet.Solution/Resources/NativeResources.cs b/VPet.Solution/Resources/NativeResources.cs new file mode 100644 index 0000000..09f7b1d --- /dev/null +++ b/VPet.Solution/Resources/NativeResources.cs @@ -0,0 +1,72 @@ +using System.Reflection; + +namespace VPet.House.Resources; + +/// +/// 本地资源 +/// +internal class NativeResources +{ + #region Resources + + public const string Wall = ResourcePath + "Wall.png"; + public const string Floor = ResourcePath + "Floor.png"; + public const string Chair = ResourcePath + "Chair.png"; + public const string Table = ResourcePath + "Table.png"; + public const string Bed = ResourcePath + "Bed.png"; + + public const string OakPlanks = ResourcePath + "oak_planks.png"; + public const string Stone = ResourcePath + "stone.png"; + + #endregion Resources + + /// + /// 资源基路径 + /// + public const string ResourcePath = $"{nameof(VPet)}.{nameof(House)}.{nameof(Resources)}."; + + #region Native + + private static readonly Assembly _assembly = Assembly.GetExecutingAssembly(); + + /// + /// 获取资源流 + /// + /// 资源名 + /// 资源流 + public static Stream GetStream(string resourceName) => + _assembly.GetManifestResourceStream(resourceName)!; + + /// + /// 尝试获取资源流 + /// + /// 资源名 + /// 资源流 + /// 成功为 失败为 + public static bool TryGetStream(string resourceName, out Stream resourceStream) + { + resourceStream = null; + if (_assembly.GetManifestResourceStream(resourceName) is not Stream stream) + return false; + resourceStream = stream; + return true; + } + + /// + /// 将流保存至文件 + /// + /// 资源名 + /// 文件路径 + /// 成功为 失败为 + public static bool SaveTo(string resourceName, string path) + { + if (_assembly.GetManifestResourceStream(resourceName) is not Stream stream) + return false; + using var sr = new StreamReader(stream); + using var sw = new StreamWriter(path); + sr.BaseStream.CopyTo(sw.BaseStream); + return true; + } + + #endregion Native +} diff --git a/VPet.Solution/SimpleObservable/ObservableClass/INotifyPropertyChangedX.cs b/VPet.Solution/SimpleObservable/ObservableClass/INotifyPropertyChangedX.cs new file mode 100644 index 0000000..141f509 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/INotifyPropertyChangedX.cs @@ -0,0 +1,12 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 通知属性改变后接口 +/// +public interface INotifyPropertyChangedX +{ + /// + /// 通知属性改变后事件 + /// + public event PropertyChangedXEventHandler? PropertyChangedX; +} diff --git a/VPet.Solution/SimpleObservable/ObservableClass/INotifyPropertyChangingX.cs b/VPet.Solution/SimpleObservable/ObservableClass/INotifyPropertyChangingX.cs new file mode 100644 index 0000000..8b47081 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/INotifyPropertyChangingX.cs @@ -0,0 +1,12 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 通知属性改变前接口 +/// +public interface INotifyPropertyChangingX +{ + /// + /// 属性改变前事件 + /// + public event PropertyChangingXEventHandler? PropertyChangingX; +} diff --git a/VPet.Solution/SimpleObservable/ObservableClass/ObservableClass.cs b/VPet.Solution/SimpleObservable/ObservableClass/ObservableClass.cs new file mode 100644 index 0000000..b8e0794 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/ObservableClass.cs @@ -0,0 +1,106 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace HKW.HKWUtils.Observable; + +/// +/// 可观察对象 +/// 示例: +/// { +/// int _value = 0; +/// public int Value +/// { +/// get => _value; +/// set => SetProperty(ref _value, value); +/// } +/// }]]> +/// +public abstract class ObservableClass + : INotifyPropertyChanging, + INotifyPropertyChanged, + INotifyPropertyChangingX, + INotifyPropertyChangedX + where TObject : ObservableClass +{ + #region OnPropertyChange + /// + /// 设置属性值 + /// + /// 值 + /// 新值 + /// 属性名称 + /// 成功为 失败为 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool SetProperty( + ref TValue value, + TValue newValue, + [CallerMemberName] string propertyName = null! + ) + { + if (EqualityComparer.Default.Equals(value, newValue) is true) + return false; + var oldValue = value; + if (OnPropertyChanging(oldValue, newValue, propertyName)) + return false; + value = newValue; + OnPropertyChanged(oldValue, newValue, propertyName); + return true; + } + + /// + /// 属性改变前 + /// + /// 旧值 + /// 新值 + /// 属性名称 + /// 取消为 否则为 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual bool OnPropertyChanging( + object? oldValue, + object? newValue, + [CallerMemberName] string propertyName = null! + ) + { + PropertyChanging?.Invoke(this, new(propertyName)); + if (PropertyChangingX is null) + return false; + var e = new PropertyChangingXEventArgs(propertyName, oldValue, newValue); + PropertyChangingX?.Invoke((TObject)this, e); + if (e.Cancel) + PropertyChanged?.Invoke(this, new(propertyName)); + return e.Cancel; + } + + /// + /// 属性改变后 + /// + /// 旧值 + /// 新值 + /// 属性名称 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected virtual void OnPropertyChanged( + object? oldValue, + object? newValue, + [CallerMemberName] string propertyName = null! + ) + { + PropertyChanged?.Invoke(this, new(propertyName)); + PropertyChangedX?.Invoke((TObject)this, new(propertyName, oldValue, newValue)); + } + #endregion + + #region Event + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public event PropertyChangingXEventHandler? PropertyChangingX; + + /// + public event PropertyChangedXEventHandler? PropertyChangedX; + #endregion +} diff --git a/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangedXEventArgs.cs b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangedXEventArgs.cs new file mode 100644 index 0000000..d9f80c2 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangedXEventArgs.cs @@ -0,0 +1,33 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 属性改变后事件参数 +/// +public class PropertyChangedXEventArgs : EventArgs +{ + /// + /// 属性名 + /// + public string PropertyName { get; } + + /// + /// 旧值 + /// + public object? OldValue { get; } + + /// + /// 新值 + /// + public object? NewValue { get; } + + /// + /// 属性名 + /// 旧值 + /// 新值 + public PropertyChangedXEventArgs(string propertyName, object? oldValue, object? newValue) + { + PropertyName = propertyName; + OldValue = oldValue; + NewValue = newValue; + } +} diff --git a/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangedXEventHandler.cs b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangedXEventHandler.cs new file mode 100644 index 0000000..178e3c9 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangedXEventHandler.cs @@ -0,0 +1,11 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 属性改变后事件 +/// +/// 发送者 +/// 参数 +public delegate void PropertyChangedXEventHandler( + TSender sender, + PropertyChangedXEventArgs e +); diff --git a/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangingXEventArgs.cs b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangingXEventArgs.cs new file mode 100644 index 0000000..bbd6eed --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangingXEventArgs.cs @@ -0,0 +1,35 @@ +using System.ComponentModel; + +namespace HKW.HKWUtils.Observable; + +/// +/// 属性改变前事件参数 +/// +public class PropertyChangingXEventArgs : CancelEventArgs +{ + /// + /// 属性名 + /// + public string PropertyName { get; } + + /// + /// 旧值 + /// + public object? OldValue { get; } + + /// + /// 新值 + /// + public object? NewValue { get; } + + /// + /// 属性名 + /// 旧值 + /// 新值 + public PropertyChangingXEventArgs(string propertyName, object? oldValue, object? newValue) + { + PropertyName = propertyName; + OldValue = oldValue; + NewValue = newValue; + } +} diff --git a/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangingXEventHandler.cs b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangingXEventHandler.cs new file mode 100644 index 0000000..9a1e472 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableClass/PropertyChangingXEventHandler.cs @@ -0,0 +1,11 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 属性改变前事件 +/// +/// 发送者 +/// 参数 +public delegate void PropertyChangingXEventHandler( + TSender sender, + PropertyChangingXEventArgs e +); diff --git a/VPet.Solution/SimpleObservable/ObservableCommand/ExecuteAsyncEventHandler.cs b/VPet.Solution/SimpleObservable/ObservableCommand/ExecuteAsyncEventHandler.cs new file mode 100644 index 0000000..07004c4 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableCommand/ExecuteAsyncEventHandler.cs @@ -0,0 +1,12 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 异步执行命令事件 +/// +public delegate Task ExecuteAsyncEventHandler(); + +/// +/// 异步执行命令事件 +/// +/// 值 +public delegate Task ExecuteAsyncEventHandler(T parameter); diff --git a/VPet.Solution/SimpleObservable/ObservableCommand/ExecuteEventHandler.cs b/VPet.Solution/SimpleObservable/ObservableCommand/ExecuteEventHandler.cs new file mode 100644 index 0000000..52075fe --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableCommand/ExecuteEventHandler.cs @@ -0,0 +1,12 @@ +namespace HKW.HKWUtils.Observable; + +/// +/// 执行事件 +/// +public delegate void ExecuteEventHandler(); + +/// +/// 执行事件 +/// +/// 参数 +public delegate void ExecuteEventHandler(T parameter); diff --git a/VPet.Solution/SimpleObservable/ObservableCommand/NotifyReceivedEventHandler.cs b/VPet.Solution/SimpleObservable/ObservableCommand/NotifyReceivedEventHandler.cs new file mode 100644 index 0000000..ba2a50e --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableCommand/NotifyReceivedEventHandler.cs @@ -0,0 +1,11 @@ +using System.ComponentModel; +using System.Windows.Input; + +namespace HKW.HKWUtils.Observable; + +/// +/// 通知接收器 +/// +/// 发送者 +/// 参数 +public delegate void NotifyReceivedEventHandler(ICommand sender, CancelEventArgs e); diff --git a/VPet.Solution/SimpleObservable/ObservableCommand/ObservableCommand.cs b/VPet.Solution/SimpleObservable/ObservableCommand/ObservableCommand.cs new file mode 100644 index 0000000..7dc7e3d --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableCommand/ObservableCommand.cs @@ -0,0 +1,115 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Windows.Input; + +namespace HKW.HKWUtils.Observable; + +/// +/// 可观察命令 +/// +[DebuggerDisplay("\\{ObservableCommand, CanExecute = {IsCanExecute.Value}\\}")] +public class ObservableCommand : ObservableClass, ICommand +{ + bool _isCanExecute = true; + + /// + /// 能执行的属性 + /// + public bool IsCanExecute + { + get => _isCanExecute; + set => SetProperty(ref _isCanExecute, value); + } + + bool _currentCanExecute = true; + + /// + /// 当前可执行状态 + /// + /// 在执行异步事件时会强制为 , 但异步结束后会恢复为 的值 + /// + /// + public bool CurrentCanExecute + { + get => _currentCanExecute; + private set => SetProperty(ref _currentCanExecute, value); + } + + /// + public ObservableCommand() + { + PropertyChanged += OnCanExecuteChanged; + } + + private void OnCanExecuteChanged(object? sender, PropertyChangedEventArgs e) + { + CanExecuteChanged?.Invoke(this, new()); + } + + #region ICommand + /// + /// 能否被执行 + /// + /// 参数 + /// 能被执行为 否则为 + public bool CanExecute(object? parameter) + { + return CurrentCanExecute && IsCanExecute; + } + + /// + /// 执行方法 + /// + /// 参数 + public async void Execute(object? parameter) + { + if (IsCanExecute is not true) + return; + ExecuteCommand?.Invoke(); + await ExecuteAsync(); + } + + /// + /// 执行异步方法, 会在等待中修改 , 完成后恢复 + /// + /// 若要在执行此方法时触发 事件, 请将 设置为 + /// + /// + /// 设置为 时触发 事件 + /// 任务 + public async Task ExecuteAsync(bool runAlone = false) + { + if (IsCanExecute is not true) + return; + if (runAlone) + ExecuteCommand?.Invoke(); + if (ExecuteAsyncCommand is null) + return; + CurrentCanExecute = false; + foreach ( + var asyncEvent in ExecuteAsyncCommand + .GetInvocationList() + .Cast() + ) + await asyncEvent.Invoke(); + CurrentCanExecute = true; + } + #endregion + + #region Event + /// + /// 能否执行属性改变后事件 + /// + public event EventHandler? CanExecuteChanged; + + /// + /// 执行事件 + /// + public event ExecuteEventHandler? ExecuteCommand; + + /// + /// 异步执行事件 + /// + public event ExecuteAsyncEventHandler? ExecuteAsyncCommand; + #endregion +} diff --git a/VPet.Solution/SimpleObservable/ObservableCommand/ObservableCommandT.cs b/VPet.Solution/SimpleObservable/ObservableCommand/ObservableCommandT.cs new file mode 100644 index 0000000..0c63112 --- /dev/null +++ b/VPet.Solution/SimpleObservable/ObservableCommand/ObservableCommandT.cs @@ -0,0 +1,116 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Windows.Input; + +namespace HKW.HKWUtils.Observable; + +/// +/// 具有参数的可观察命令 +/// +[DebuggerDisplay("\\{ObservableCommand, CanExecute = {IsCanExecute.Value}\\}")] +public class ObservableCommand : ObservableClass, ICommand +{ + bool _isCanExecute = true; + + /// + /// 能执行的属性 + /// + public bool IsCanExecute + { + get => _isCanExecute; + set => SetProperty(ref _isCanExecute, value); + } + + bool _currentCanExecute = true; + + /// + /// 当前可执行状态 + /// + /// 在执行异步事件时会强制为 , 但异步结束后会恢复为 的值 + /// + /// + public bool CurrentCanExecute + { + get => _currentCanExecute; + private set => SetProperty(ref _currentCanExecute, value); + } + + /// + public ObservableCommand() + { + PropertyChanged += OnCanExecuteChanged; + } + + private void OnCanExecuteChanged(object? sender, PropertyChangedEventArgs e) + { + CanExecuteChanged?.Invoke(this, new()); + } + + #region ICommand + /// + /// 能否被执行 + /// + /// 参数 + /// 能被执行为 否则为 + public bool CanExecute(object? parameter) + { + return CurrentCanExecute && IsCanExecute; + } + + /// + /// 执行方法 + /// + /// 参数 + public async void Execute(object? parameter) + { + if (IsCanExecute is not true) + return; + ExecuteCommand?.Invoke((T)parameter!); + await ExecuteAsync((T)parameter!); + } + + /// + /// 执行异步方法, 会在等待中修改 , 完成后恢复 + /// + /// 若要在执行此方法时触发 事件, 请将 设置为 + /// + /// + /// 参数 + /// 设置为 时触发 事件 + /// 任务 + public async Task ExecuteAsync(T parameter, bool runAlone = false) + { + if (IsCanExecute is not true) + return; + if (runAlone) + ExecuteCommand?.Invoke(parameter); + if (ExecuteAsyncCommand is null) + return; + CurrentCanExecute = false; + foreach ( + var asyncEvent in ExecuteAsyncCommand + .GetInvocationList() + .Cast>() + ) + await asyncEvent.Invoke(parameter); + CurrentCanExecute = true; + } + #endregion + + #region Event + /// + /// 能否执行属性改变后事件 + /// + public event EventHandler? CanExecuteChanged; + + /// + /// 执行事件 + /// + public event ExecuteEventHandler? ExecuteCommand; + + /// + /// 异步执行事件 + /// + public event ExecuteAsyncEventHandler? ExecuteAsyncCommand; + #endregion +} diff --git a/VPet.Solution/Styles.xaml b/VPet.Solution/Styles.xaml new file mode 100644 index 0000000..0b1c5b7 --- /dev/null +++ b/VPet.Solution/Styles.xaml @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VPet.Solution/Templates.xaml b/VPet.Solution/Templates.xaml new file mode 100644 index 0000000..ac14da7 --- /dev/null +++ b/VPet.Solution/Templates.xaml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VPet.Solution/Usings.cs b/VPet.Solution/Usings.cs new file mode 100644 index 0000000..ae09c80 --- /dev/null +++ b/VPet.Solution/Usings.cs @@ -0,0 +1,7 @@ +global using global::HKW.HKWUtils; +global using global::HKW.HKWUtils.Observable; +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Threading.Tasks; diff --git a/VPet.Solution/Utils/ClearFocus.cs b/VPet.Solution/Utils/ClearFocus.cs new file mode 100644 index 0000000..f5a06dc --- /dev/null +++ b/VPet.Solution/Utils/ClearFocus.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; + +namespace HKW.WPF.Extensions; + +public static partial class WPFExtensions +{ + /// + /// 清除元素焦点 + /// + /// 元素 + public static void ClearFocus(this DependencyObject obj) + { + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(obj), null); + } +} diff --git a/VPet.Solution/Utils/ElementHelper.cs b/VPet.Solution/Utils/ElementHelper.cs new file mode 100644 index 0000000..d270394 --- /dev/null +++ b/VPet.Solution/Utils/ElementHelper.cs @@ -0,0 +1,271 @@ +using HKW.WPF.Extensions; +using System.Windows; +using System.Windows.Data; +using System.Windows.Input; + +namespace HKW.WPF.Helpers; + +/// +/// +/// +public static class ElementHelper +{ + #region IsEnabled + /// + /// + /// + /// + /// + public static bool GetIsEnabled(FrameworkElement element) + { + return (bool)element.GetValue(IsEnabledProperty); + } + + /// + /// + /// + public static void SetIsEnabled(FrameworkElement element, bool value) + { + element.SetValue(IsEnabledProperty, value); + } + + /// + /// 在按下指定按键时清除选中状态 + /// + public static readonly DependencyProperty IsEnabledProperty = + DependencyProperty.RegisterAttached( + "IsEnabled", + typeof(bool), + typeof(ElementHelper), + new FrameworkPropertyMetadata(default(bool), IsEnabledPropertyChangedCallback) + ); + + private static void IsEnabledPropertyChangedCallback( + DependencyObject obj, + DependencyPropertyChangedEventArgs e + ) + { + if (obj is not FrameworkElement element) + return; + element.IsEnabled = GetIsEnabled(element); + } + #endregion + + #region ClearFocusOnKeyDown + /// + /// + /// + /// + /// + public static string GetClearFocusOnKeyDown(FrameworkElement element) + { + return (string)element.GetValue(ClearFocusOnKeyDownProperty); + } + + /// + /// + /// + /// 禁止使用此方法 + public static void SetClearFocusOnKeyDown(FrameworkElement element, string value) + { + element.SetValue(ClearFocusOnKeyDownProperty, value); + } + + /// + /// 在按下指定按键时清除选中状态 + /// + public static readonly DependencyProperty ClearFocusOnKeyDownProperty = + DependencyProperty.RegisterAttached( + "ClearFocusOnKeyDown", + typeof(string), + typeof(ElementHelper), + new FrameworkPropertyMetadata(default(string), ClearFocusOnKeyDownPropertyChanged) + ); + + private static void ClearFocusOnKeyDownPropertyChanged( + DependencyObject obj, + DependencyPropertyChangedEventArgs e + ) + { + if (obj is not FrameworkElement element) + return; + var keyName = GetClearFocusOnKeyDown(element); + if (Enum.TryParse(keyName, false, out _) is false) + throw new Exception($"Unknown key {keyName}"); + element.KeyDown -= Element_KeyDown; + element.KeyDown += Element_KeyDown; + + static void Element_KeyDown(object sender, KeyEventArgs e) + { + if (sender is not FrameworkElement element) + return; + var key = (Key)Enum.Parse(typeof(Key), GetClearFocusOnKeyDown(element)); + if (e.Key == key) + { + // 清除控件焦点 + element.ClearFocus(); + // 清除键盘焦点 + Keyboard.ClearFocus(); + } + } + } + #endregion + + #region UniformMinWidthGroup + /// + /// + /// + /// + /// + public static string GetUniformMinWidthGroup(FrameworkElement element) + { + return (string)element.GetValue(UniformWidthGroupProperty); + } + + /// + /// + /// + public static void SetUniformMinWidthGroup(FrameworkElement element, string value) + { + element.SetValue(UniformWidthGroupProperty, value); + } + + /// + /// + /// + public static readonly DependencyProperty UniformWidthGroupProperty = + DependencyProperty.RegisterAttached( + "UniformMinWidthGroup", + typeof(string), + typeof(ElementHelper), + new FrameworkPropertyMetadata(null, UniformMinWidthGroupPropertyChanged) + ); + + /// + /// (TopParent ,(GroupName, UniformMinWidthGroupInfo)) + /// + private readonly static Dictionary< + FrameworkElement, + Dictionary + > _uniformMinWidthGroups = new(); + + private static void UniformMinWidthGroupPropertyChanged( + DependencyObject obj, + DependencyPropertyChangedEventArgs e + ) + { + if (obj is not FrameworkElement element) + return; + var groupName = GetUniformMinWidthGroup(element); + var topParent = element.FindTopParentOnVisualTree(); + // 在设计器中会无法获取顶级元素, 会提示错误, 忽略即可 + if (topParent is null) + return; + if (_uniformMinWidthGroups.TryGetValue(topParent, out var groups) is false) + { + topParent.Dispatcher.ShutdownStarted += Dispatcher_ShutdownStarted; + groups = _uniformMinWidthGroups[topParent] = new(); + } + if (groups.TryGetValue(groupName, out var group) is false) + group = groups[groupName] = new(); + group.Elements.Add(element); + + element.SizeChanged -= Element_SizeChanged; + element.SizeChanged += Element_SizeChanged; + + #region TopParent + + static void Dispatcher_ShutdownStarted(object? sender, EventArgs e) + { + if (sender is not FrameworkElement element) + return; + _uniformMinWidthGroups.Remove(element); + } + #endregion + + #region Element + static void Element_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (sender is not FrameworkElement element) + return; + var groupName = GetUniformMinWidthGroup(element); + var topParent = element.FindTopParentOnVisualTree(); + var groups = _uniformMinWidthGroups[topParent]; + var group = groups[groupName]; + var maxWidthElement = group.Elements.MaxBy(i => i.ActualWidth); + if (maxWidthElement is null) + return; + + if (maxWidthElement.ActualWidth == element.ActualWidth) + maxWidthElement = element; + if (maxWidthElement.ActualWidth > group.MaxWidth) + { + // 如果当前控件最大宽度的超过历史最大宽度, 表明非最大宽度列表中的控件超过了历史最大宽度 + foreach (var item in group.Elements) + item.MinWidth = maxWidthElement.ActualWidth; + // 将当前控件最小宽度设为0 + maxWidthElement.MinWidth = 0; + group.MaxWidthElements.Clear(); + // 设为最大宽度的唯一控件 + group.MaxWidthElements.Add(maxWidthElement); + group.MaxWidth = maxWidthElement.ActualWidth; + } + else if (group.MaxWidthElements.Count == 1) + { + maxWidthElement = group.MaxWidthElements.First(); + // 当最大宽度控件只有一个时, 并且当前控件宽度小于历史最大宽度时, 表明需要降低全体宽度 + if (group.MaxWidth > maxWidthElement.ActualWidth) + { + // 最小宽度设为0以自适应宽度 + foreach (var item in group.Elements) + item.MinWidth = 0; + // 清空最大宽度列表, 让其刷新 + group.MaxWidthElements.Clear(); + } + } + else + { + // 将 MaxWidth 设置为 double.MaxValue 时, 可以让首次加载时进入此处 + foreach (var item in group.Elements) + { + // 当控件最小宽度为0(表示其为主导宽度的控件), 并且其宽度等于最大宽度, 加入最大宽度列表 + if (item.MinWidth == 0 && item.ActualWidth == maxWidthElement.ActualWidth) + { + group.MaxWidthElements.Add(item); + } + else + { + // 如果不是, 则从最大宽度列表删除, 并设置最小宽度为当前最大宽度 + group.MaxWidthElements.Remove(item); + item.MinWidth = maxWidthElement.ActualWidth; + } + } + group.MaxWidth = maxWidthElement.ActualWidth; + } + } + #endregion + } + + #endregion +} + +/// +/// 统一最小宽度分组信息 +/// +public class UniformMinWidthGroupInfo +{ + /// + /// 最后一个最大宽度 + /// + public double MaxWidth { get; set; } = double.MaxValue; + + /// + /// 所有控件 + /// + public HashSet Elements { get; } = new(); + + /// + /// 最大宽度的控件 + /// + public HashSet MaxWidthElements { get; } = new(); +} diff --git a/VPet.Solution/Utils/Expansions.cs b/VPet.Solution/Utils/Expansions.cs new file mode 100644 index 0000000..a08259d --- /dev/null +++ b/VPet.Solution/Utils/Expansions.cs @@ -0,0 +1,357 @@ +using System.Collections; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace HKW.HKWUtils; + +/// +/// 拓展 +/// +public static class Extensions +{ + /// + /// + /// + /// + /// + /// + /// + public static bool Contains(this string source, string value, StringComparison comparisonType) + { + return source.IndexOf(value, comparisonType) >= 0; + } + + public static TSource MaxBy( + this IEnumerable source, + Func keySelector + ) + { + using IEnumerator e = source.GetEnumerator(); + if (e.MoveNext() is false) + return default; + TSource value = e.Current; + TKey key = keySelector(value); + while (e.MoveNext()) + { + TSource nextValue = e.Current; + TKey nextKey = keySelector(nextValue); + if (Comparer.Default.Compare(nextKey, key) > 0) + { + key = nextKey; + value = nextValue; + } + } + return value; + } + + //public static string GetSourceFile(this BitmapImage image) + //{ + // return ((FileStream)image.StreamSource).Name; + //} + + /// + /// 关闭流 + /// + /// 图像资源 + public static void CloseStream(this ImageSource source) + { + if (source is not BitmapImage image) + return; + image.StreamSource?.Close(); + } + + /// + /// 图像复制 + /// + /// 图像 + /// 复制的图像 + public static BitmapImage Copy(this BitmapImage image) + { + if (image is null) + return null; + BitmapImage newImage = new(); + newImage.BeginInit(); + newImage.DecodePixelWidth = image.DecodePixelWidth; + newImage.DecodePixelHeight = image.DecodePixelHeight; + try + { + using var bitmap = new Bitmap(image.StreamSource); + var ms = new MemoryStream(); + bitmap.Save(ms, ImageFormat.Png); + image.StreamSource.CopyTo(ms); + newImage.StreamSource = ms; + } + finally + { + newImage.EndInit(); + } + return newImage; + } + + /// + /// 保存至Png图片 + /// + /// 图片资源 + /// 路径 + //public static void SaveToPng(this BitmapImage image, string path) + //{ + // if (image is null) + // return; + // if (path.EndsWith(".png") is false) + // path += ".png"; + // var encoder = new PngBitmapEncoder(); + // var stream = image.StreamSource; + // // 保存位置 + // var position = stream.Position; + // // 必须要重置位置, 否则EndInit将出错 + // stream.Seek(0, SeekOrigin.Begin); + // encoder.Frames.Add(BitmapFrame.Create(image.StreamSource)); + // // 恢复位置 + // stream.Seek(position, SeekOrigin.Begin); + // using var fs = new FileStream(path, FileMode.Create); + // encoder.Save(fs); + //} + public static void SaveToPng(this BitmapImage image, string path) + { + if (image is null) + return; + if (path.EndsWith(".png") is false) + path += ".png"; + var stream = image.StreamSource; + // 保存位置 + var position = stream.Position; + // 必须要重置位置, 否则EndInit将出错 + stream.Seek(0, SeekOrigin.Begin); + using var fs = new FileStream(path, FileMode.Create); + stream.CopyTo(fs); + // 恢复位置 + stream.Seek(position, SeekOrigin.Begin); + } + + /// + /// 尝试添加 + /// + /// 键类型 + /// + /// + /// 键 + /// 值 + /// 成功为 失败为 + public static bool TryAdd( + this IDictionary dictionary, + TKey key, + TValue value + ) + { + if (dictionary.ContainsKey(key)) + return false; + dictionary.Add(key, value); + return true; + } + + /// + /// 流内容对比 + /// + /// 原始流 + /// 目标流 + /// 缓冲区大小 (越大速度越快(流内容越大效果越明显), 但会提高内存占用 (bufferSize = bufferLength * sizeof(long) * 2)) + /// 内容相同为 否则为 + public static bool ContentsEqual(this Stream source, Stream target, int bufferLength = 8) + { + int bufferSize = bufferLength * sizeof(long); + var sourceBuffer = new byte[bufferSize]; + var targetBuffer = new byte[bufferSize]; + while (true) + { + int sourceCount = ReadFullBuffer(source, sourceBuffer); + int targetCount = ReadFullBuffer(target, targetBuffer); + if (sourceCount != targetCount) + return false; + if (sourceCount == 0) + return true; + + for (int i = 0; i < sourceCount; i += sizeof(long)) + if (BitConverter.ToInt64(sourceBuffer, i) != BitConverter.ToInt64(targetBuffer, i)) + return false; + } + static int ReadFullBuffer(Stream stream, byte[] buffer) + { + int bytesRead = 0; + while (bytesRead < buffer.Length) + { + int read = stream.Read(buffer, bytesRead, buffer.Length - bytesRead); + if (read == 0) + return bytesRead; + bytesRead += read; + } + return bytesRead; + } + } + + public static T? FindVisualChild(this DependencyObject obj) + where T : DependencyObject + { + if (obj is null) + return null; + var count = VisualTreeHelper.GetChildrenCount(obj); + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(obj, i); + if (child is T t) + return t; + if (FindVisualChild(child) is T childItem) + return childItem; + } + return null; + } + + public static T FindParent(this DependencyObject obj) + where T : class + { + while (obj != null) + { + if (obj is T) + return obj as T; + obj = VisualTreeHelper.GetParent(obj); + } + return null; + } + + public static string GetFullInfo(this CultureInfo cultureInfo) + { + return $"{cultureInfo.DisplayName} [{cultureInfo.Name}]"; + } + + /// + /// 尝试使用索引获取值 + /// + /// 值类型 + /// 列表 + /// 索引 + /// 值 + /// 成功为 失败为 + public static bool TryGetValue(this IList list, int index, out T value) + { + value = default; + if (index < 0 || index >= list.Count) + return false; + value = list[index]; + return true; + } + + /// + /// 尝试使用索引获取值 + /// + /// 值类型 + /// 列表 + /// 索引 + /// 值 + /// 成功为 失败为 + public static bool TryGetValue(this IList list, int index, out object value) + { + value = default; + if (index < 0 || index >= list.Count) + return false; + value = list[index]; + return true; + } + + /// + /// 获取目标 + /// + /// 类型 + /// 弱引用 + /// 获取成功返回目标值, 获取失败则返回 + public static T? GetTarget(this WeakReference weakReference) + where T : class + { + return weakReference.TryGetTarget(out var t) ? t : null; + } + + /// + /// 枚举出带有索引值的枚举值 + /// + /// 值类型 + /// 集合 + /// 带有索引的枚举值 + public static IEnumerable> Enumerate(this IEnumerable collection) + { + var index = 0; + foreach (var item in collection) + yield return new(index++, item); + } + + /// + /// 设置视图模型 + /// + /// 视图模型类型 + /// 窗口 + public static Lazy SetViewModel(this Window window, EventHandler? closedEvent = null) + where T : new() + { + if (window.DataContext is null) + { + window.DataContext = new T(); + window.Closed += (s, e) => + { + try + { + window.DataContext = null; + } + catch { } + }; + window.Closed += closedEvent; + } + return new(() => (T)window.DataContext); + } + + /// + /// 设置视图模型 + /// + /// 视图模型类型 + /// 页面 + public static Lazy SetViewModel(this Page page) + where T : new() + { + return new(() => (T)(page.DataContext ??= new T())); + } +} + +/// +/// 项信息 +/// +/// +[DebuggerDisplay("[{Index}, {Value}]")] +public readonly struct ItemInfo +{ + /// + /// 索引值 + /// + public int Index { get; } + + /// + /// 值 + /// + public T Value { get; } + + /// + /// 值 + /// 索引值 + public ItemInfo(int index, T value) + { + Index = index; + Value = value; + } + + /// + public override string ToString() + { + return $"[{Index}, {Value}]"; + } +} diff --git a/VPet.Solution/Utils/FindTopParent.cs b/VPet.Solution/Utils/FindTopParent.cs new file mode 100644 index 0000000..f848c2b --- /dev/null +++ b/VPet.Solution/Utils/FindTopParent.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace HKW.WPF.Extensions; + +public static partial class WPFExtensions +{ + /// + /// 判断是否是顶级元素, 它必须是 , , 中的一个 + /// + /// 元素 + public static bool IsTopParent(this FrameworkElement element) + { + if (element is Window || element is Page || element is UserControl) + return true; + return false; + } + + /// + /// 寻找它的顶级元素, 它肯定是 , , 中的一个 + /// + /// 元素 + public static FrameworkElement FindTopParent(this FrameworkElement element) + { + if (element.IsTopParent()) + return element; + var parent = element.Parent as FrameworkElement; + while (parent is not null) + { + if (parent.IsTopParent()) + return parent; + parent = parent.Parent as FrameworkElement; + } + return null!; + } + + /// + /// 尝试寻找它的顶级元素, 它肯定是 , , 中的一个 + /// + /// 元素 + /// 顶级元素 + /// 成功为 失败为 + public static bool TryFindTopParent( + this FrameworkElement element, + out FrameworkElement topParent + ) + { + var result = element.FindTopParent(); + if (result is null) + { + topParent = null; + return false; + } + topParent = result; + return true; + } + + /// + /// 在视觉树上寻找它的顶级元素, 它肯定是 , , 中的一个 + /// + /// 用于 无法获取到顶级元素的情况下使用, 此元素通常位于 中 + /// + /// + /// 元素 + public static FrameworkElement FindTopParentOnVisualTree(this FrameworkElement element) + { + if (element.TryFindTopParent(out var top)) + return top; + var temp = (DependencyObject)element; + while ((temp = VisualTreeHelper.GetParent(temp)) is not null) + { + if (temp is FrameworkElement fe && fe.TryFindTopParent(out var topParent)) + return topParent; + } + return null!; + } + + /// + /// 尝试在视觉树上寻找它的顶级元素, 它肯定是 , , 中的一个 + /// + /// 元素 + /// 顶级元素 + /// 成功为 失败为 + public static bool FindTopParentOnVisualTree( + this FrameworkElement element, + out FrameworkElement topParent + ) + { + var result = element.FindTopParentOnVisualTree(); + if (result is null) + { + topParent = null; + return false; + } + topParent = result; + return true; + } + + /// + /// 寻找它的顶级元素, 它肯定是 , , 中的一个 + /// + /// 元素 + public static TParent FindTopParent(this FrameworkElement element) + where TParent : FrameworkElement + { + var type = typeof(TParent); + if (type != typeof(Window) && type != typeof(Page) && type != typeof(UserControl)) + throw new Exception("TParent type must be Window, Page or UserControl"); + return (TParent)FindTopParent(element); + } +} diff --git a/VPet.Solution/Utils/HashCode.cs b/VPet.Solution/Utils/HashCode.cs new file mode 100644 index 0000000..b88ff89 --- /dev/null +++ b/VPet.Solution/Utils/HashCode.cs @@ -0,0 +1,66 @@ +namespace HKW.HKWUtils; + +/// +/// 哈希值 +/// +public class HashCode +{ + /// + /// 默认种子 + /// + public const int DefaultSeed = 114514; + + /// + /// 默认系数 + /// + public const int DefaultFactor = 1919810; + + /// + /// 组合哈希值 + /// + /// 值 + /// 组合的哈希值 + public static int Combine(params object[] values) + { + return CustomHash(DefaultSeed, DefaultFactor, values.Select(v => v.GetHashCode())); + } + + /// + /// 组合哈希值 + /// + /// 种子 + /// 系数 + /// 值 + /// 组合的哈希值 + public static int Combine(int seed, int factor, params object[] values) + { + return CustomHash(seed, factor, values.Select(v => v.GetHashCode())); + } + + /// + /// 自定义组合哈希 + /// + /// 种子 + /// 系数 + /// 哈希集合 + /// 组合的哈希 + public static int CustomHash(int seed, int factor, IEnumerable collection) + { + int hash = seed; + foreach (int i in collection) + hash = unchecked((hash * factor) + i); + return hash; + } + + /// + /// 自定义组合哈希 + /// + /// 种子 + /// 系数 + /// 哈希集合 + /// 组合的哈希 + public static int CustomHash(int seed, int factor, params int[] values) + { + return CustomHash(seed, factor, collection: values); + } +} diff --git a/VPet.Solution/Utils/ObservableEnumFlags.cs b/VPet.Solution/Utils/ObservableEnumFlags.cs new file mode 100644 index 0000000..3e863ce --- /dev/null +++ b/VPet.Solution/Utils/ObservableEnumFlags.cs @@ -0,0 +1,72 @@ +namespace HKW.HKWUtils; + +/// +/// 可观察的枚举标签模型 +/// +/// 枚举类型 +public class ObservableEnumFlags : ObservableClass> + where T : Enum +{ + private T _EnumValue; + public T EnumValue + { + get => _EnumValue; + set => SetProperty(ref _EnumValue, value); + } + + /// + /// 添加枚举命令 + /// + public ObservableCommand AddCommand { get; } = new(); + + /// + /// 删除枚举命令 + /// + public ObservableCommand RemoveCommand { get; } = new(); + + /// + /// 枚举类型 + /// + public Type EnumType = typeof(T); + + /// + /// 枚举基类 + /// + public Type UnderlyingType { get; } = Enum.GetUnderlyingType(typeof(T)); + + public ObservableEnumFlags() + { + if (Attribute.IsDefined(EnumType, typeof(FlagsAttribute)) is false) + throw new Exception($"此枚举类型未使用特性 [{nameof(FlagsAttribute)}]"); + AddCommand.ExecuteCommand += AddCommand_Execute; + RemoveCommand.ExecuteCommand += RemoveCommand_Execute; + } + + public ObservableEnumFlags(T value) + : this() + { + EnumValue = value; + } + + private void AddCommand_Execute(T v) + { + if (UnderlyingType == typeof(int)) + { + EnumValue = (T) + Enum.Parse(EnumType, (Convert.ToInt32(EnumValue) | Convert.ToInt32(v)).ToString()); + } + else + throw new NotImplementedException($"Value type: {UnderlyingType}"); + } + + private void RemoveCommand_Execute(T v) + { + if (UnderlyingType == typeof(int)) + { + EnumValue = (T) + Enum.Parse(EnumType, (Convert.ToInt32(EnumValue) & ~Convert.ToInt32(v)).ToString()); + } + else + throw new NotImplementedException($"Value type: {UnderlyingType}"); + } +} diff --git a/VPet.Solution/Utils/ObservablePoint.cs b/VPet.Solution/Utils/ObservablePoint.cs new file mode 100644 index 0000000..eff89d6 --- /dev/null +++ b/VPet.Solution/Utils/ObservablePoint.cs @@ -0,0 +1,77 @@ +namespace HKW.HKWUtils; + +/// +/// 可观察地点 +/// +/// 类型 +public class ObservablePoint + : ObservableClass>, + IEquatable> +{ + private T _x; + public T X + { + get => _x; + set => SetProperty(ref _x, value); + } + + private T _y; + public T Y + { + get => _y; + set => SetProperty(ref _y, value); + } + + public ObservablePoint() { } + + public ObservablePoint(T x, T y) + { + X = x; + Y = y; + } + + /// + /// 复制一个新的对象 + /// + /// 新对象 + public ObservablePoint Copy() + { + return new(X, Y); + } + + #region Other + + /// + public override int GetHashCode() + { + return HashCode.Combine(X, Y); + } + + /// + public override bool Equals(object? obj) + { + return obj is ObservablePoint temp + && EqualityComparer.Default.Equals(X, temp.X) + && EqualityComparer.Default.Equals(Y, temp.Y); + } + + /// + public bool Equals(ObservablePoint? other) + { + return Equals(obj: other); + } + + /// + public static bool operator ==(ObservablePoint a, ObservablePoint b) + { + return Equals(a, b); + } + + /// + public static bool operator !=(ObservablePoint a, ObservablePoint b) + { + return Equals(a, b) is not true; + } + + #endregion +} diff --git a/VPet.Solution/Utils/ObservableRange.cs b/VPet.Solution/Utils/ObservableRange.cs new file mode 100644 index 0000000..76a1d6f --- /dev/null +++ b/VPet.Solution/Utils/ObservableRange.cs @@ -0,0 +1,77 @@ +namespace HKW.HKWUtils; + +/// +/// 可观察的范围 +/// +/// 类型 +public class ObservableRange + : ObservableClass>, + IEquatable> +{ + private T _min; + public T Min + { + get => _min; + set => SetProperty(ref _min, value); + } + + private T _max; + public T Max + { + get => _max; + set => SetProperty(ref _max, value); + } + + public ObservableRange() { } + + public ObservableRange(T min, T max) + { + _min = min; + _max = max; + } + + /// + /// 复制一个新的对象 + /// + /// 新对象 + public ObservableRange Copy() + { + return new(Min, Max); + } + + #region Other + + /// + public override int GetHashCode() + { + return HashCode.Combine(Min, Max); + } + + /// + public override bool Equals(object? obj) + { + return obj is ObservableRange temp + && EqualityComparer.Default.Equals(Min, temp.Min) + && EqualityComparer.Default.Equals(Max, temp.Max); + } + + /// + public bool Equals(ObservableRange? other) + { + return Equals(obj: other); + } + + /// + public static bool operator ==(ObservableRange a, ObservableRange b) + { + return Equals(a, b); + } + + /// + public static bool operator !=(ObservableRange a, ObservableRange b) + { + return Equals(a, b) is not true; + } + + #endregion +} diff --git a/VPet.Solution/Utils/ObservableRect.cs b/VPet.Solution/Utils/ObservableRect.cs new file mode 100644 index 0000000..49924a7 --- /dev/null +++ b/VPet.Solution/Utils/ObservableRect.cs @@ -0,0 +1,89 @@ +namespace HKW.HKWUtils; + +public class ObservableRect : ObservableClass>, IEquatable> +{ + private T _x; + public T X + { + get => _x; + set => SetProperty(ref _x, value); + } + + private T _y; + public T Y + { + get => _y; + set => SetProperty(ref _y, value); + } + + private T _width; + public T Width + { + get => _width; + set => SetProperty(ref _width, value); + } + + private T _heigth; + public T Height + { + get => _heigth; + set => SetProperty(ref _heigth, value); + } + + public ObservableRect() { } + + public ObservableRect(T x, T y, T width, T hetght) + { + X = x; + Y = y; + Width = width; + Height = hetght; + } + + /// + /// 复制一个新的对象 + /// + /// 新对象 + public ObservableRect Copy() + { + return new(X, Y, Width, Height); + } + + #region Other + + /// + public override int GetHashCode() + { + return HashCode.Combine(X, Y, Width, Height); + } + + /// + public override bool Equals(object? obj) + { + return obj is ObservableRect temp + && EqualityComparer.Default.Equals(X, temp.X) + && EqualityComparer.Default.Equals(Y, temp.Y) + && EqualityComparer.Default.Equals(Width, temp.Width) + && EqualityComparer.Default.Equals(Height, temp.Height); + } + + /// + public bool Equals(ObservableRect? other) + { + return Equals(obj: other); + } + + /// + public static bool operator ==(ObservableRect a, ObservableRect b) + { + return Equals(a, b); + } + + /// + public static bool operator !=(ObservableRect a, ObservableRect b) + { + return Equals(a, b) is not true; + } + + #endregion +} diff --git a/VPet.Solution/Utils/Utils.cs b/VPet.Solution/Utils/Utils.cs new file mode 100644 index 0000000..c628c1b --- /dev/null +++ b/VPet.Solution/Utils/Utils.cs @@ -0,0 +1,79 @@ +using System.Windows.Media.Imaging; + +namespace HKW.HKWUtils; + +/// +/// 工具 +/// +public static class Utils +{ + /// + /// 解码像素宽度 + /// + public const int DecodePixelWidth = 250; + + /// + /// 解码像素高度 + /// + public const int DecodePixelHeight = 250; + public static char[] Separator { get; } = new char[] { '_' }; + + //public static BitmapImage LoadImageToStream(string imagePath) + //{ + // BitmapImage bitmapImage = new(); + // bitmapImage.BeginInit(); + // bitmapImage.DecodePixelWidth = DecodePixelWidth; + // try + // { + // bitmapImage.StreamSource = new StreamReader(imagePath).BaseStream; + // } + // finally + // { + // bitmapImage.EndInit(); + // } + // return bitmapImage; + //} + + /// + /// 载入图片至内存流 + /// + /// 图片路径 + /// + public static BitmapImage LoadImageToMemoryStream(string imagePath) + { + BitmapImage bitmapImage = new(); + bitmapImage.BeginInit(); + try + { + var bytes = File.ReadAllBytes(imagePath); + bitmapImage.StreamSource = new MemoryStream(bytes); + bitmapImage.DecodePixelWidth = DecodePixelWidth; + } + finally + { + bitmapImage.EndInit(); + } + return bitmapImage; + } + + /// + /// 载入图片至内存流 + /// + /// 图片流 + /// + public static BitmapImage LoadImageToMemoryStream(Stream imageStream) + { + BitmapImage bitmapImage = new(); + bitmapImage.BeginInit(); + try + { + bitmapImage.StreamSource = imageStream; + bitmapImage.DecodePixelWidth = DecodePixelWidth; + } + finally + { + bitmapImage.EndInit(); + } + return bitmapImage; + } +} diff --git a/VPet.Solution/VPet.Solution.csproj b/VPet.Solution/VPet.Solution.csproj index daa478f..0524d43 100644 --- a/VPet.Solution/VPet.Solution.csproj +++ b/VPet.Solution/VPet.Solution.csproj @@ -14,6 +14,8 @@ 4 true true + enable + latest AnyCPU @@ -39,7 +41,8 @@ ..\packages\LinePutScript.1.9.2\lib\net462\LinePutScript.dll - ..\packages\LinePutScript.Localization.WPF.1.0.6\lib\net462\LinePutScript.Localization.WPF.dll + + ..\packages\LinePutScript.Localization.WPF.1.0.6\lib\net462\LinePutScript.Localization.WPF.dll ..\packages\Panuon.WPF.1.0.2\lib\net462\Panuon.WPF.dll @@ -49,6 +52,7 @@ + @@ -67,7 +71,55 @@ MSBuild:Compile Designer - + + + + + + + + + + CustomizedSettingsPage.xaml + + + DiagnosticSettingsPage.xaml + + + ModSettingsPage.xaml + + + SystemSettingsPage.xaml + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + MSBuild:Compile Designer @@ -75,10 +127,67 @@ App.xaml Code - - MainWindow.xaml - Code + + + + + GraphicsSettingsPage.xaml + + InteractiveSettingsPage.xaml + + + MainWindow.xaml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + Designer + true + + + NativeStyles.xaml + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + @@ -117,5 +226,8 @@ VPet-Simulator.Windows.Interface + + + \ No newline at end of file diff --git a/VPet.Solution/ViewModels/CustomizedSettingsPageVM.cs b/VPet.Solution/ViewModels/CustomizedSettingsPageVM.cs new file mode 100644 index 0000000..965acfb --- /dev/null +++ b/VPet.Solution/ViewModels/CustomizedSettingsPageVM.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VPet.Solution.ViewModels; + +public class CustomizedSettingsPageVM { } diff --git a/VPet.Solution/ViewModels/DiagnosticSettingsPageVM.cs b/VPet.Solution/ViewModels/DiagnosticSettingsPageVM.cs new file mode 100644 index 0000000..683ebe5 --- /dev/null +++ b/VPet.Solution/ViewModels/DiagnosticSettingsPageVM.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VPet.Solution.ViewModels; + +public class DiagnosticSettingsPageVM { } diff --git a/VPet.Solution/ViewModels/GraphicsSettingsPageVM.cs b/VPet.Solution/ViewModels/GraphicsSettingsPageVM.cs new file mode 100644 index 0000000..2609ab4 --- /dev/null +++ b/VPet.Solution/ViewModels/GraphicsSettingsPageVM.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VPet.Solution.ViewModels; + +public class GraphicsSettingsPageVM { } diff --git a/VPet.Solution/ViewModels/InteractiveSettingsPageVM.cs b/VPet.Solution/ViewModels/InteractiveSettingsPageVM.cs new file mode 100644 index 0000000..1b7fbb6 --- /dev/null +++ b/VPet.Solution/ViewModels/InteractiveSettingsPageVM.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VPet.Solution.ViewModels; + +public class InteractiveSettingsPageVM { } diff --git a/VPet.Solution/ViewModels/MainWindowVM.cs b/VPet.Solution/ViewModels/MainWindowVM.cs new file mode 100644 index 0000000..3f2ed0e --- /dev/null +++ b/VPet.Solution/ViewModels/MainWindowVM.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VPet_Simulator.Core; +using VPet_Simulator.Windows.Interface; + +namespace VPet.Solution.ViewModels; + +public class MainWindowVM : ObservableClass +{ + public MainWindowVM() { } + + public static void LoadSettings(string path) + { + foreach (var file in Directory.EnumerateFiles(path)) + { + var setting = new Setting(path); + } + } +} diff --git a/VPet.Solution/ViewModels/ModSettingsPageVM.cs b/VPet.Solution/ViewModels/ModSettingsPageVM.cs new file mode 100644 index 0000000..bef412e --- /dev/null +++ b/VPet.Solution/ViewModels/ModSettingsPageVM.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VPet.Solution.ViewModels; + +public class ModSettingsPageVM { } diff --git a/VPet.Solution/ViewModels/SystemSettingsPageVM.cs b/VPet.Solution/ViewModels/SystemSettingsPageVM.cs new file mode 100644 index 0000000..842b6d0 --- /dev/null +++ b/VPet.Solution/ViewModels/SystemSettingsPageVM.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace VPet.Solution.ViewModels; + +public class SystemSettingsPageVM { } diff --git a/VPet.Solution/Views/CustomizedSettingsPage.xaml b/VPet.Solution/Views/CustomizedSettingsPage.xaml new file mode 100644 index 0000000..b9fec7e --- /dev/null +++ b/VPet.Solution/Views/CustomizedSettingsPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + +