├── .gitattributes
├── .gitignore
├── README.md
├── WpfTypingPractice.sln
├── WpfTypingPractice
├── App.config
├── App.xaml
├── App.xaml.cs
├── FodyWeavers.xml
├── FodyWeavers.xsd
├── Fonts
│ └── HanYiJiaShuJian.ttf
├── Helpers
│ └── StringHelper.cs
├── Images
│ └── typing.ico
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── SimpleHelpers
│ ├── FileEncoding.cs
│ └── ShuangpinScheme.cs
├── Styles
│ ├── Colors.xaml
│ └── Fonts.xaml
├── ViewModels
│ ├── BaseViewModel.cs
│ ├── MainWindowViewModel.cs
│ └── RelayCommand.cs
├── WpfTypingPractice.csproj
├── packages.config
└── pinyinKeys.json
└── demo.png
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 打字练习软件
2 |
3 | 一款基于 WPF 的打字练习软件,可以练习全拼和双拼。效果如下图所示:
4 |
5 | 
6 |
7 | 软件完全遵循 MVVM 架构模式。
8 |
9 | 使用的 NuGet 包:
10 |
11 | 1. [PropertyChanged.Fody](https://github.com/Fody/PropertyChanged)
12 | 2. [Newtonsoft.Json](https://www.newtonsoft.com/json)
13 | 3. [PinYinConverterCore](https://github.com/netcorepal/PinYinConverterCore)
14 | 4. [UDE.CSharp](http://code.google.com/p/ude/)
15 |
--------------------------------------------------------------------------------
/WpfTypingPractice.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30128.74
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfTypingPractice", "WpfTypingPractice\WpfTypingPractice.csproj", "{C6684AF6-013A-40C5-818F-6D47AF746590}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {C6684AF6-013A-40C5-818F-6D47AF746590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {C6684AF6-013A-40C5-818F-6D47AF746590}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {C6684AF6-013A-40C5-818F-6D47AF746590}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {C6684AF6-013A-40C5-818F-6D47AF746590}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {A74DABD9-069B-4763-8F12-D95A6B91B9ED}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/WpfTypingPractice/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/WpfTypingPractice/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/WpfTypingPractice/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 |
9 | namespace WpfTypingPractice
10 | {
11 | ///
12 | /// App.xaml 的交互逻辑
13 | ///
14 | public partial class App : Application
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/WpfTypingPractice/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/WpfTypingPractice/FodyWeavers.xsd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Used to control if the On_PropertyName_Changed feature is enabled.
12 |
13 |
14 |
15 |
16 | Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.
17 |
18 |
19 |
20 |
21 | Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.
22 |
23 |
24 |
25 |
26 | Used to control if equality checks should use the Equals method resolved from the base class.
27 |
28 |
29 |
30 |
31 | Used to control if equality checks should use the static Equals method resolved from the base class.
32 |
33 |
34 |
35 |
36 | Used to turn off build warnings from this weaver.
37 |
38 |
39 |
40 |
41 | Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
50 |
51 |
52 |
53 |
54 | A comma-separated list of error codes that can be safely ignored in assembly verification.
55 |
56 |
57 |
58 |
59 | 'false' to turn off automatic generation of the XML Schema file.
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Fonts/HanYiJiaShuJian.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BYJRK/WpfTypingPractice/ed636046e91017e0b1c59ca89c61bdf18308a567/WpfTypingPractice/Fonts/HanYiJiaShuJian.ttf
--------------------------------------------------------------------------------
/WpfTypingPractice/Helpers/StringHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.SymbolStore;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using Microsoft.International.Converters.PinYinConverter;
7 | using Newtonsoft.Json;
8 | using Newtonsoft.Json.Linq;
9 | using Ude;
10 | using WpfTypingPractice.SimpleHelpers;
11 |
12 | namespace WpfTypingPractice.Helpers
13 | {
14 | public static class StringHelper
15 | {
16 | private static JObject pinyinDict;
17 |
18 | ///
19 | /// 当前的双拼方案
20 | ///
21 | public static ShuangpinScheme ShuangpinMode { get; set; } = ShuangpinScheme.MSShuangpin;
22 |
23 | ///
24 | /// 获取当前的双拼映射
25 | ///
26 | public static JObject GetShuangpinMap()
27 | {
28 | if (pinyinDict != null)
29 | {
30 | return (JObject)pinyinDict[ShuangpinMode.ToString().ToLower()]["map"];
31 | }
32 | return null;
33 | }
34 |
35 | static StringHelper()
36 | {
37 | // 读取外部的双拼键位映射
38 | var textReader = new StreamReader("pinyinKeys.json");
39 | var jsonReader = new JsonTextReader(textReader);
40 | pinyinDict = JObject.Load(jsonReader);
41 | }
42 |
43 | ///
44 | /// 判断一个字符是否是具有拼音的有效中文字符
45 | ///
46 | ///
47 | ///
48 | public static bool IsValidChineseChar(char ch)
49 | {
50 | return ChineseChar.IsValidChar(ch);
51 | }
52 |
53 | ///
54 | /// 将一个拼音拆分为 (声母, 韵母)
55 | ///
56 | ///
57 | ///
58 | public static (string consonant, string vowel) SplitConVow(string pinyin)
59 | {
60 | // 如果是双声母开头
61 | if (pinyin.StartsWithAny("zh", "ch", "sh"))
62 | {
63 | return (pinyin.Substring(0, 2), pinyin.Substring(2));
64 | }
65 | // 以一些韵母开头
66 | else if (pinyin.StartsWithAny("a", "e", "o"))
67 | {
68 | return ("", pinyin);
69 | }
70 | // 否则是单声母+韵母
71 | else
72 | {
73 | return (pinyin.Substring(0, 1), pinyin.Substring(1));
74 | }
75 | }
76 |
77 | ///
78 | /// 获取中文单字的拼音
79 | ///
80 | ///
81 | /// 所有可能的拼音组成的字符串数组
82 | public static string[] GetPinyins(char ch)
83 | {
84 | if (!IsValidChineseChar(ch))
85 | {
86 | return null;
87 | }
88 | ChineseChar cc = new ChineseChar(ch);
89 | return cc.Pinyins.Where(p => !string.IsNullOrEmpty(p)).Select(p => p.ToLower().Substring(0, p.Length - 1)).ToArray();
90 | }
91 |
92 | ///
93 | /// 获取拼音对应的双拼
94 | ///
95 | ///
96 | ///
97 | public static string GetShuangpin(string pinyin)
98 | {
99 | // 我们假定传入的拼音是正确有效的
100 | var convow = SplitConVow(pinyin);
101 | var con = convow.consonant;
102 | var vow = convow.vowel;
103 |
104 | var map = GetShuangpinMap();
105 |
106 | // 如果没有声母
107 | if (string.IsNullOrEmpty(con))
108 | {
109 | return map["vowelstart"].ToString() + GetShuangpinKey(vow);
110 | }
111 |
112 | // 表明是声母+韵母
113 | return GetShuangpinKey(con) + GetShuangpinKey(vow);
114 | }
115 |
116 | ///
117 | /// 获取单个音对应的双拼按键
118 | ///
119 | ///
120 | ///
121 | public static string GetShuangpinKey(string syllable)
122 | {
123 | if (syllable.Length == 1 && syllable != "v")
124 | {
125 | return syllable;
126 | }
127 |
128 | var map = GetShuangpinMap();
129 | // 遍历所有键位映射
130 | foreach (var pair in map)
131 | {
132 | if (pair.Value.Select(p => p.ToString()).Contains(syllable))
133 | {
134 | return pair.Key;
135 | }
136 | }
137 | return null;
138 | }
139 |
140 | ///
141 | /// 检测一个外部文件的编码
142 | ///
143 | ///
144 | ///
145 | public static Encoding GetEncoding(string path)
146 | {
147 | using (var fs = File.OpenRead(path))
148 | {
149 | CharsetDetector detector = new CharsetDetector();
150 | detector.Feed(fs);
151 | detector.DataEnd();
152 | if (!string.IsNullOrEmpty(detector.Charset))
153 | return Encoding.GetEncoding(detector.Charset);
154 | }
155 |
156 | return null;
157 | }
158 |
159 | ///
160 | /// 判断一个字符串的开头是否符合一组字符串中的任意一个
161 | ///
162 | ///
163 | ///
164 | ///
165 | public static bool StartsWithAny(this string text, params string[] patterns)
166 | {
167 | return patterns.Any(p => text.StartsWith(p));
168 | }
169 |
170 | ///
171 | /// 判断字符串是否与一组字符串中任意一个相同
172 | ///
173 | ///
174 | ///
175 | ///
176 | public static bool EqualsAny(this string text, params string[] others)
177 | {
178 | foreach (var s in others)
179 | {
180 | if (text == s)
181 | return true;
182 | }
183 | return false;
184 | }
185 |
186 | ///
187 | /// 自动将文章的段落分隔更换为统一的间隔空行
188 | ///
189 | ///
190 | ///
191 | public static string AutoInsertEmptyLine(string text)
192 | {
193 | string linebreak = "\r\n";
194 | string newline = Environment.NewLine;
195 | if (!text.Contains(linebreak))
196 | linebreak = "\n";
197 | // 清除所有连续的换行符
198 | while (text.Contains(linebreak + linebreak))
199 | {
200 | text = text.Replace(linebreak + linebreak, linebreak);
201 | }
202 | return text.Replace(linebreak, newline + newline);
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Images/typing.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BYJRK/WpfTypingPractice/ed636046e91017e0b1c59ca89c61bdf18308a567/WpfTypingPractice/Images/typing.ico
--------------------------------------------------------------------------------
/WpfTypingPractice/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
44 |
84 |
85 |
86 |
87 |
88 |
89 |
94 |
100 |
104 |
105 |
106 |
107 |
108 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
128 |
129 |
130 |
134 |
142 |
143 |
149 |
150 |
151 |
153 |
154 |
155 |
156 |
157 |
158 |
163 |
164 |
166 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
179 |
180 |
187 |
188 |
189 |
190 |
191 |
--------------------------------------------------------------------------------
/WpfTypingPractice/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using WpfTypingPractice.Helpers;
3 | using WpfTypingPractice.ViewModels;
4 |
5 | namespace WpfTypingPractice
6 | {
7 | ///
8 | /// MainWindow.xaml 的交互逻辑
9 | ///
10 | public partial class MainWindow : Window
11 | {
12 | public MainWindow()
13 | {
14 | InitializeComponent();
15 |
16 | this.DataContext = new MainWindowViewModel();
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // 有关程序集的一般信息由以下
8 | // 控制。更改这些特性值可修改
9 | // 与程序集关联的信息。
10 | [assembly: AssemblyTitle("TypingPractice")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("TypingPractice")]
15 | [assembly: AssemblyCopyright("Copyright © 2020")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // 将 ComVisible 设置为 false 会使此程序集中的类型
20 | //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
21 | //请将此类型的 ComVisible 特性设置为 true。
22 | [assembly: ComVisible(false)]
23 |
24 | //若要开始生成可本地化的应用程序,请设置
25 | //.csproj 文件中的 CultureYouAreCodingWith
26 | //例如,如果您在源文件中使用的是美国英语,
27 | //使用的是美国英语,请将 设置为 en-US。 然后取消
28 | //对以下 NeutralResourceLanguage 特性的注释。 更新
29 | //以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //主题特定资源词典所处位置
36 | //(未在页面中找到资源时使用,
37 | //或应用程序资源字典中找到时使用)
38 | ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
39 | //(未在页面中找到资源时使用,
40 | //、应用程序或任何主题专用资源字典中找到时使用)
41 | )]
42 |
43 |
44 | // 程序集的版本信息由下列四个值组成:
45 | //
46 | // 主版本
47 | // 次版本
48 | // 生成号
49 | // 修订号
50 | //
51 | //可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
52 | //通过使用 "*",如下所示:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("1.0.0.0")]
55 | [assembly: AssemblyFileVersion("1.0.0.0")]
56 | [assembly: NeutralResourcesLanguage("zh-Hans")]
57 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // 此代码由工具生成。
4 | // 运行时版本:4.0.30319.42000
5 | //
6 | // 对此文件的更改可能会导致不正确的行为,并且如果
7 | // 重新生成代码,这些更改将会丢失。
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WpfTypingPractice.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// 一个强类型的资源类,用于查找本地化的字符串等。
17 | ///
18 | // 此类是由 StronglyTypedResourceBuilder
19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
21 | // (以 /str 作为命令选项),或重新生成 VS 项目。
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// 返回此类使用的缓存的 ResourceManager 实例。
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WpfTypingPractice.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// 重写当前线程的 CurrentUICulture 属性
51 | /// 重写当前线程的 CurrentUICulture 属性。
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // 此代码由工具生成。
4 | // 运行时版本:4.0.30319.42000
5 | //
6 | // 对此文件的更改可能会导致不正确的行为,并且如果
7 | // 重新生成代码,这些更改将会丢失。
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WpfTypingPractice.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.6.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/WpfTypingPractice/SimpleHelpers/FileEncoding.cs:
--------------------------------------------------------------------------------
1 | #region * License *
2 | /*
3 | SimpleHelpers - FileEncoding
4 |
5 | Copyright © 2014 Khalid Salomão
6 |
7 | Permission is hereby granted, free of charge, to any person
8 | obtaining a copy of this software and associated documentation
9 | files (the “Software”), to deal in the Software without
10 | restriction, including without limitation the rights to use,
11 | copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the
13 | Software is furnished to do so, subject to the following
14 | conditions:
15 |
16 | The above copyright notice and this permission notice shall be
17 | included in all copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | OTHER DEALINGS IN THE SOFTWARE.
27 |
28 | License: http://www.opensource.org/licenses/mit-license.php
29 | Website: https://github.com/khalidsalomao/SimpleHelpers.Net
30 | */
31 | #endregion
32 |
33 | using System;
34 | using System.IO;
35 | using System.Collections.Generic;
36 | using System.Linq;
37 | using System.Text;
38 |
39 | namespace WpfTypingPractice.SimpleHelpers
40 | {
41 | public class FileEncoding
42 | {
43 | const int DEFAULT_BUFFER_SIZE = 128 * 1024;
44 |
45 | ///
46 | /// Tries to detect the file encoding.
47 | ///
48 | /// The input filename.
49 | /// The default encoding if none was detected.
50 | ///
51 | public static Encoding DetectFileEncoding (string inputFilename, Encoding defaultIfNotDetected = null)
52 | {
53 | using (var stream = new System.IO.FileStream (inputFilename, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete, DEFAULT_BUFFER_SIZE))
54 | {
55 | return DetectFileEncoding (stream) ?? defaultIfNotDetected;
56 | }
57 | }
58 |
59 | ///
60 | /// Tries to detect the file encoding.
61 | ///
62 | /// The input stream.
63 | /// The default encoding if none was detected.
64 | ///
65 | public static Encoding DetectFileEncoding (Stream inputStream, Encoding defaultIfNotDetected = null)
66 | {
67 | var det = new FileEncoding ();
68 | det.Detect (inputStream);
69 | return det.Complete () ?? defaultIfNotDetected;
70 | }
71 |
72 | ///
73 | /// Tries to detect the file encoding.
74 | ///
75 | /// The input data.
76 | /// The start.
77 | /// The count.
78 | /// The default encoding if none was detected.
79 | ///
80 | public static Encoding DetectFileEncoding (byte[] inputData, int start, int count, Encoding defaultIfNotDetected = null)
81 | {
82 | var det = new FileEncoding ();
83 | det.Detect (inputData, start, count);
84 | return det.Complete () ?? defaultIfNotDetected;
85 | }
86 |
87 | ///
88 | /// Tries to load file content with the correct encoding.
89 | ///
90 | /// The filename.
91 | /// The default value if unable to load file content.
92 | /// File content
93 | public static string TryLoadFile (string filename, string defaultValue = "")
94 | {
95 | try
96 | {
97 | if (System.IO.File.Exists (filename))
98 | {
99 | // enable file encoding detection
100 | var encoding = SimpleHelpers.FileEncoding.DetectFileEncoding (filename);
101 | // Load data based on parameters
102 | return System.IO.File.ReadAllText (filename, encoding);
103 | }
104 | }
105 | catch { }
106 | return defaultValue;
107 | }
108 |
109 | ///
110 | /// Detects if contains textual data.
111 | ///
112 | /// The raw data.
113 | public static bool CheckForTextualData (byte[] rawData)
114 | {
115 | return CheckForTextualData (rawData, 0, rawData.Length);
116 | }
117 |
118 | ///
119 | /// Detects if contains textual data.
120 | ///
121 | /// The raw data.
122 | /// The start.
123 | /// The count.
124 | public static bool CheckForTextualData (byte[] rawData, int start, int count)
125 | {
126 | if (rawData.Length < count || count < 4 || start + 1 >= count)
127 | return true;
128 |
129 | if (CheckForByteOrderMark (rawData, start))
130 | {
131 | return true;
132 | }
133 |
134 | // http://stackoverflow.com/questions/910873/how-can-i-determine-if-a-file-is-binary-or-text-in-c
135 | // http://www.gnu.org/software/diffutils/manual/html_node/Binary.html
136 | // count the number od null bytes sequences
137 | // considering only sequeces of 2 0s: "\0\0" or control characters below 10
138 | int nullSequences = 0;
139 | int controlSequences = 0;
140 | for (var i = start + 1; i < count; i++)
141 | {
142 | if (rawData[i - 1] == 0 && rawData[i] == 0)
143 | {
144 | if (++nullSequences > 1)
145 | break;
146 | }
147 | else if (rawData[i - 1] == 0 && rawData[i] < 10)
148 | {
149 | ++controlSequences;
150 | }
151 | }
152 |
153 | // is text if there is no null byte sequences or less than 10% of the buffer has control caracteres
154 | return nullSequences == 0 && (controlSequences <= (rawData.Length / 10));
155 | }
156 |
157 | ///
158 | /// Detects if data has bytes order mark to indicate its encoding for textual data.
159 | ///
160 | /// The raw data.
161 | /// The start.
162 | ///
163 | private static bool CheckForByteOrderMark (byte[] rawData, int start = 0)
164 | {
165 | if (rawData.Length - start < 4)
166 | return false;
167 | // Detect encoding correctly (from Rick Strahl's blog)
168 | // http://www.west-wind.com/weblog/posts/2007/Nov/28/Detecting-Text-Encoding-for-StreamReader
169 | if (rawData[start] == 0xef && rawData[start + 1] == 0xbb && rawData[start + 2] == 0xbf)
170 | {
171 | // Encoding.UTF8;
172 | return true;
173 | }
174 | else if (rawData[start] == 0xfe && rawData[start + 1] == 0xff)
175 | {
176 | // Encoding.Unicode;
177 | return true;
178 | }
179 | else if (rawData[start] == 0 && rawData[start + 1] == 0 && rawData[start + 2] == 0xfe && rawData[start + 3] == 0xff)
180 | {
181 | // Encoding.UTF32;
182 | return true;
183 | }
184 | else if (rawData[start] == 0x2b && rawData[start + 1] == 0x2f && rawData[start + 2] == 0x76)
185 | {
186 | // Encoding.UTF7;
187 | return true;
188 | }
189 | return false;
190 | }
191 |
192 | Ude.CharsetDetector ude = new Ude.CharsetDetector ();
193 | bool _started = false;
194 |
195 |
196 | ///
197 | /// If the detection has reached a decision.
198 | ///
199 | /// The done.
200 | public bool Done { get; set; }
201 |
202 | ///
203 | /// Detected encoding name.
204 | ///
205 | public string EncodingName { get; set; }
206 |
207 | ///
208 | /// If the data contains textual data.
209 | ///
210 | public bool IsText { get; set; }
211 |
212 | ///
213 | /// If the file or data has any mark indicating encoding information (byte order mark).
214 | ///
215 | public bool HasByteOrderMark { get; set; }
216 |
217 | Dictionary encodingFrequency = new Dictionary (StringComparer.Ordinal);
218 |
219 | ///
220 | /// Resets this instance.
221 | ///
222 | public void Reset ()
223 | {
224 | _started = false;
225 | Done = false;
226 | HasByteOrderMark = false;
227 | encodingFrequency.Clear ();
228 | ude.Reset ();
229 | EncodingName = null;
230 | }
231 |
232 | ///
233 | /// Detects the encoding of textual data of the specified input data.
234 | /// Only the stream first 20Mb will be analysed.
235 | ///
236 | /// The input data.
237 | /// Detected encoding name
238 | public string Detect (Stream inputData)
239 | {
240 | return Detect (inputData, 20 * 1024 * 1024);
241 | }
242 |
243 | ///
244 | /// Detects the encoding of textual data of the specified input data.
245 | ///
246 | /// The input data.
247 | /// Size in byte of analysed data, if you want to analysed only a sample. Use 0 to read all stream data.
248 | /// Size of the buffer for the stream read.
249 | /// Detected encoding name
250 | /// bufferSize parameter cannot be 0 or less.
251 | public string Detect (Stream inputData, int maxSize, int bufferSize = 16 * 1024)
252 | {
253 | if (bufferSize <= 0)
254 | throw new ArgumentOutOfRangeException ("bufferSize", "Buffer size cannot be 0 or less.");
255 | int maxIterations = maxSize <= 0 ? Int32.MaxValue : maxSize / bufferSize;
256 | int i = 0;
257 | byte[] buffer = new byte[bufferSize];
258 | while (i++ < maxIterations)
259 | {
260 | int sz = inputData.Read (buffer, 0, (int)buffer.Length);
261 | if (sz <= 0)
262 | {
263 | break;
264 | }
265 | Detect (buffer, 0, sz);
266 | if (Done)
267 | break;
268 | }
269 | Complete ();
270 | return EncodingName;
271 | }
272 |
273 | ///
274 | /// Detects the encoding of textual data of the specified input data.
275 | ///
276 | /// The input data.
277 | /// The start.
278 | /// The count.
279 | /// Detected encoding name
280 | public string Detect (byte[] inputData, int start, int count)
281 | {
282 | if (Done)
283 | return EncodingName;
284 | if (!_started)
285 | {
286 | Reset ();
287 | _started = true;
288 | if (!CheckForTextualData (inputData, start, count))
289 | {
290 | IsText = false;
291 | Done = true;
292 | return EncodingName;
293 | }
294 | HasByteOrderMark = CheckForByteOrderMark (inputData, start);
295 | IsText = true;
296 | }
297 |
298 | // execute charset detector
299 | ude.Feed (inputData, start, count);
300 | ude.DataEnd ();
301 | if (ude.IsDone () && !String.IsNullOrEmpty (ude.Charset))
302 | {
303 | IncrementFrequency (ude.Charset);
304 | Done = true;
305 | return EncodingName;
306 | }
307 |
308 | // singular buffer detection
309 | var singleUde = new Ude.CharsetDetector ();
310 | const int udeFeedSize = 4 * 1024;
311 | int step = (count - start) < udeFeedSize ? (count - start) : udeFeedSize;
312 | for (var pos = start; pos < count; pos += step)
313 | {
314 | singleUde.Reset ();
315 | if (pos + step > count)
316 | singleUde.Feed (inputData, pos, count - pos);
317 | else
318 | singleUde.Feed (inputData, pos, step);
319 | singleUde.DataEnd ();
320 | // update encoding frequency
321 | if (singleUde.Confidence > 0.3 && !String.IsNullOrEmpty (singleUde.Charset))
322 | IncrementFrequency (singleUde.Charset);
323 | }
324 | // vote for best encoding
325 | EncodingName = GetCurrentEncoding ();
326 | // update current encoding name
327 | return EncodingName;
328 | }
329 |
330 | ///
331 | /// Finalize detection phase and gets detected encoding name.
332 | ///
333 | ///
334 | public Encoding Complete ()
335 | {
336 | Done = true;
337 | ude.DataEnd ();
338 | if (ude.IsDone () && !String.IsNullOrEmpty (ude.Charset))
339 | {
340 | EncodingName = ude.Charset;
341 | }
342 | // vote for best encoding
343 | EncodingName = GetCurrentEncoding ();
344 | // check result
345 | if (!String.IsNullOrEmpty (EncodingName))
346 | return Encoding.GetEncoding (EncodingName);
347 | return null;
348 | }
349 |
350 | private void IncrementFrequency (string charset)
351 | {
352 | int currentCount;
353 | encodingFrequency.TryGetValue (charset, out currentCount);
354 | encodingFrequency[charset] = ++currentCount;
355 | }
356 |
357 | private string GetCurrentEncoding ()
358 | {
359 | if (encodingFrequency.Count == 0)
360 | return null;
361 | // ASCII should be the last option, since other encodings often has ASCII included...
362 | return encodingFrequency
363 | .OrderByDescending (i => i.Value * (i.Key != ("ASCII") ? 1 : 0))
364 | .FirstOrDefault ().Key;
365 | }
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/WpfTypingPractice/SimpleHelpers/ShuangpinScheme.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace WpfTypingPractice.SimpleHelpers
8 | {
9 | ///
10 | /// 双拼键位方案
11 | ///
12 | public enum ShuangpinScheme
13 | {
14 | ///
15 | /// 微软双拼
16 | ///
17 | MSShuangpin
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Styles/Colors.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | #051923
8 |
9 |
10 | #003554
11 |
12 |
13 | #006494
14 |
15 |
16 | #00006494
17 |
18 |
19 | #0582CA
20 |
21 |
22 | #00A6FB
23 |
24 |
--------------------------------------------------------------------------------
/WpfTypingPractice/Styles/Fonts.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 | pack://application:,,,/Fonts/#汉仪家书简
6 |
7 |
--------------------------------------------------------------------------------
/WpfTypingPractice/ViewModels/BaseViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace WpfTypingPractice.ViewModels
4 | {
5 | public class BaseViewModel : INotifyPropertyChanged
6 | {
7 | public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/WpfTypingPractice/ViewModels/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Win32;
2 | using PropertyChanged;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Windows;
8 | using System.Windows.Input;
9 | using WpfTypingPractice.Helpers;
10 |
11 | namespace WpfTypingPractice.ViewModels
12 | {
13 | public class MainWindowViewModel : BaseViewModel
14 | {
15 | public MainWindowViewModel()
16 | {
17 | MainGridMouseClickCommand = new RelayCommand(OpenTxtFile);
18 | WindowKeyOpenFileCommand = new RelayCommand(OpenTxtFile);
19 | WindowEscCommand = new RelayCommand(() =>
20 | {
21 | Input = "";
22 | });
23 | InputModeButtonCommand = new RelayCommand(() =>
24 | {
25 | IsUsingShuangpin = !IsUsingShuangpin;
26 | });
27 |
28 | //Article = Finished = "双击上方任意位置打开外部文本";
29 | }
30 |
31 | #region 与打字相关
32 |
33 | private int currentIndex = 0;
34 |
35 | ///
36 | /// 开始打字前的准备工作
37 | ///
38 | public void GetReady()
39 | {
40 | Finished = "";
41 | currentIndex = 0;
42 | TypedWordCount = 0;
43 | WordCount = Article.Where(c => StringHelper.IsValidChineseChar(c)).Count();
44 | SkipInvalidChars();
45 | }
46 |
47 | ///
48 | /// 跳过之后的所有无需练习的内容
49 | ///
50 | private void SkipInvalidChars()
51 | {
52 | for (int i = currentIndex; i < Article.Length; i++)
53 | {
54 | if (StringHelper.IsValidChineseChar(Article[i]))
55 | break;
56 | Finished += Article[currentIndex++];
57 | }
58 | }
59 |
60 | private bool IsFinished
61 | {
62 | get => Article == Finished;
63 | }
64 |
65 | public bool CanInput
66 | {
67 | get => !IsFinished;
68 | }
69 |
70 | private string input;
71 | ///
72 | /// 用户输入框,控制打字的逻辑
73 | ///
74 | public string Input
75 | {
76 | get { return input; }
77 | set
78 | {
79 | // 如果已经完成,则不响应
80 | if (IsFinished)
81 | {
82 | input = "";
83 | return;
84 | }
85 |
86 | // 如果输入的是空白,那么会清空
87 | if (string.IsNullOrWhiteSpace(value))
88 | {
89 | input = "";
90 | return;
91 | }
92 |
93 | char cur = Article[currentIndex];
94 | string[] pinyins = StringHelper.GetPinyins(cur);
95 | if (IsUsingShuangpin)
96 | pinyins = pinyins.Select(p => StringHelper.GetShuangpin(p)).ToArray();
97 | // 如果输入的是正确的拼音
98 | if (value.ToLower().EqualsAny(pinyins))
99 | {
100 | Finished += cur;
101 | currentIndex++;
102 | TypedWordCount++;
103 | SkipInvalidChars();
104 | input = "";
105 | CalculateSpeed();
106 | return;
107 | }
108 | // 如果输入的拼音长度超过最长的可能拼音
109 | if (value.ToLower().Length > pinyins.Max(p => p.Length))
110 | {
111 | input = "";
112 | return;
113 | }
114 | input = value;
115 | }
116 | }
117 |
118 | ///
119 | /// 是否使用双拼
120 | ///
121 | [AlsoNotifyFor(nameof(InputMode))]
122 | public bool IsUsingShuangpin { get; set; } = false;
123 |
124 | #endregion
125 |
126 | #region 速度和字数统计
127 |
128 | ///
129 | /// 统计每次打字的时刻
130 | ///
131 | private LinkedList keyPressTicks = new LinkedList();
132 | ///
133 | /// 最大的时间间隔
134 | ///
135 | private TimeSpan maxTimeSpan = new TimeSpan(0, 0, 10);
136 | ///
137 | /// 最长的用来计算速度的时刻数量
138 | ///
139 | private int maxLength = 30;
140 |
141 | ///
142 | /// 打字速度
143 | ///
144 | public int TypingSpeed { get; set; } = 0;
145 |
146 | ///
147 | /// 已经打过的字数统计
148 | ///
149 | [AlsoNotifyFor(nameof(WordCountInfo))]
150 | public int TypedWordCount { get; set; } = 0;
151 |
152 | ///
153 | /// 总字数
154 | ///
155 | [AlsoNotifyFor(nameof(WordCountInfo))]
156 | public int WordCount { get; set; } = 0;
157 |
158 | private void CalculateSpeed()
159 | {
160 | var cur = DateTime.Now;
161 | // 如果时间列表是空的
162 | if (keyPressTicks.Count == 0)
163 | {
164 | keyPressTicks.AddLast(cur);
165 | TypingSpeed = 0;
166 | return;
167 | }
168 | var last = keyPressTicks.Last.Value;
169 | // 如果距离上次的时间太久,则前面的全部作废
170 | if (cur - last > maxTimeSpan)
171 | {
172 | keyPressTicks.Clear();
173 | keyPressTicks.AddLast(cur);
174 | TypingSpeed = 0;
175 | return;
176 | }
177 |
178 | keyPressTicks.AddLast(cur);
179 | // 如果储存过多,则删掉前面的
180 | while (keyPressTicks.Count > maxLength)
181 | {
182 | keyPressTicks.RemoveFirst();
183 | }
184 | // 如果元素超过一个,则计算速度
185 | if (keyPressTicks.Count > 1)
186 | {
187 | var timespan = (keyPressTicks.Last.Value - keyPressTicks.First.Value).TotalSeconds;
188 | TypingSpeed = (int)((keyPressTicks.Count - 1) / timespan * 60);
189 | return;
190 | }
191 |
192 | TypingSpeed = 0;
193 | }
194 |
195 | #endregion
196 |
197 | #region 界面显示的绑定
198 |
199 | public string ArticleTitle { get; set; }
200 |
201 | ///
202 | /// 文章标题是否可见
203 | ///
204 | [AlsoNotifyFor(nameof(WordCountVisibility))]
205 | public Visibility TitleVisibility
206 | {
207 | get
208 | {
209 | if (string.IsNullOrEmpty(ArticleTitle))
210 | return Visibility.Collapsed;
211 | else
212 | return Visibility.Visible;
213 | }
214 | }
215 |
216 | ///
217 | /// 中央提示信息是否可见
218 | ///
219 | public Visibility NoticeMessageVisibility
220 | {
221 | get
222 | {
223 | if (string.IsNullOrWhiteSpace(Article))
224 | return Visibility.Visible;
225 | else
226 | return Visibility.Hidden;
227 | }
228 | }
229 |
230 | ///
231 | /// 字数统计信息是否可见
232 | ///
233 | public Visibility WordCountVisibility
234 | {
235 | get => TitleVisibility;
236 | }
237 |
238 | ///
239 | /// 字数统计信息
240 | ///
241 | public string WordCountInfo
242 | {
243 | get
244 | {
245 | return $"{TypedWordCount}/{WordCount}";
246 | }
247 | }
248 |
249 | ///
250 | /// 用来练习打字的文本
251 | ///
252 | [AlsoNotifyFor(nameof(NoticeMessageVisibility))]
253 | public string Article { get; set; }
254 |
255 | ///
256 | /// 已经打过的内容
257 | ///
258 | public string Finished { get; set; }
259 |
260 | ///
261 | /// 当前的输入方式
262 | ///
263 | public string InputMode
264 | {
265 | get
266 | {
267 | if (IsUsingShuangpin)
268 | return "双拼";
269 | else
270 | return "全拼";
271 | }
272 | }
273 |
274 | #endregion
275 |
276 | #region 元件绑定的 Command
277 |
278 | ///
279 | /// Ctrl+O 打开外部文件
280 | ///
281 | public ICommand WindowKeyOpenFileCommand { get; set; }
282 |
283 | ///
284 | /// 双击屏幕上方文字部分,打开外部文件
285 | ///
286 | public ICommand MainGridMouseClickCommand { get; set; }
287 |
288 | ///
289 | /// 按下 ESC,清空输入框
290 | ///
291 | public ICommand WindowEscCommand { get; set; }
292 |
293 | ///
294 | /// 切换输入方式的按钮
295 | ///
296 | public ICommand InputModeButtonCommand { get; set; }
297 |
298 | #endregion
299 |
300 | #region 其他方法
301 |
302 | ///
303 | /// 打开外部文本文件
304 | ///
305 | public void OpenTxtFile()
306 | {
307 | OpenFileDialog open = new OpenFileDialog
308 | {
309 | Filter = "文本文件|*.txt"
310 | };
311 | if (open.ShowDialog().Value)
312 | {
313 | var filename = open.FileName;
314 | // 检测文本文件的编码
315 | var encoding = StringHelper.GetEncoding(filename);
316 |
317 | // 无法获取编码,放弃读取
318 | if (encoding is null)
319 | {
320 | MessageBox.Show("无法识别文本文件的文本编码。", "提示");
321 | return;
322 | }
323 |
324 | // 开始读取
325 | using (var reader = new StreamReader(filename, encoding))
326 | {
327 | var article = reader.ReadToEnd().Trim();
328 |
329 | // 读取的文本文件内容是空的(不具有用来练习的文本内容)
330 | if (string.IsNullOrWhiteSpace(article))
331 | {
332 | MessageBox.Show("外部文本内容是空的。请选择其他文本文件。", "提示");
333 | return;
334 | }
335 |
336 | Article = StringHelper.AutoInsertEmptyLine(article);
337 | ArticleTitle = Path.GetFileNameWithoutExtension(filename);
338 | if (ArticleTitle.Length > 30)
339 | ArticleTitle = ArticleTitle.Substring(0, 30) + "……";
340 |
341 | GetReady();
342 | }
343 | }
344 | }
345 |
346 | #endregion
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/WpfTypingPractice/ViewModels/RelayCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Runtime.Remoting.Channels;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using System.Windows.Input;
8 |
9 | namespace WpfTypingPractice.ViewModels
10 | {
11 | public class RelayCommand : ICommand
12 | {
13 | private Action action;
14 |
15 | public RelayCommand(Action action)
16 | {
17 | this.action = action;
18 | }
19 |
20 | public event EventHandler CanExecuteChanged = (sender, e) => { };
21 |
22 | public bool CanExecute(object parameter)
23 | {
24 | return true;
25 | }
26 |
27 | public void Execute(object parameter)
28 | {
29 | action();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/WpfTypingPractice/WpfTypingPractice.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Debug
7 | AnyCPU
8 | {C6684AF6-013A-40C5-818F-6D47AF746590}
9 | WinExe
10 | WpfTypingPractice
11 | TypingPractice
12 | v4.7.2
13 | 512
14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
15 | 4
16 | true
17 | true
18 |
19 |
20 |
21 |
22 |
23 | AnyCPU
24 | true
25 | full
26 | false
27 | bin\Debug\
28 | DEBUG;TRACE
29 | prompt
30 | 4
31 |
32 |
33 | AnyCPU
34 | pdbonly
35 | true
36 | bin\Release\
37 | TRACE
38 | prompt
39 | 4
40 |
41 |
42 | Images\typing.ico
43 |
44 |
45 |
46 | ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll
47 |
48 |
49 | ..\packages\PinYinConverterCore.1.0.2\lib\net45\PinYinConverterCore.dll
50 |
51 |
52 | ..\packages\PropertyChanged.Fody.3.2.8\lib\net40\PropertyChanged.dll
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | 4.0
64 |
65 |
66 | ..\packages\UDE.CSharp.1.1.0\lib\Ude.dll
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | MSBuild:Compile
75 | Designer
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | MSBuild:Compile
85 | Designer
86 |
87 |
88 | App.xaml
89 | Code
90 |
91 |
92 | MainWindow.xaml
93 | Code
94 |
95 |
96 | Designer
97 | MSBuild:Compile
98 |
99 |
100 | Designer
101 | MSBuild:Compile
102 |
103 |
104 |
105 |
106 | Code
107 |
108 |
109 | True
110 | True
111 | Resources.resx
112 |
113 |
114 | True
115 | Settings.settings
116 | True
117 |
118 |
119 | ResXFileCodeGenerator
120 | Resources.Designer.cs
121 |
122 |
123 |
124 |
125 | SettingsSingleFileGenerator
126 | Settings.Designer.cs
127 |
128 |
129 |
130 | PreserveNewest
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | 这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/WpfTypingPractice/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/WpfTypingPractice/pinyinKeys.json:
--------------------------------------------------------------------------------
1 | {
2 | "msshuangpin": {
3 | "map": {
4 | "q": [ "iu" ],
5 | "w": [ "ia", "ua" ],
6 | "e": [],
7 | "r": [ "uan", "er" ],
8 | "t": [ "ve", "ue" ],
9 | "y": [ "uai", "v" ],
10 | "u": [ "sh" ],
11 | "i": [ "ch" ],
12 | "o": [ "uo" ],
13 | "p": [ "un" ],
14 | "a": [],
15 | "s": [ "ong", "iong" ],
16 | "d": [ "iang", "uang" ],
17 | "f": [ "en" ],
18 | "g": [ "eng" ],
19 | "h": [ "ang" ],
20 | "j": [ "an" ],
21 | "k": [ "ao" ],
22 | "l": [ "ai" ],
23 | ";": [ "ing" ],
24 | "z": [ "ei" ],
25 | "x": [ "ie" ],
26 | "c": [ "iao" ],
27 | "v": [ "ui", "zh" ],
28 | "b": [ "ou" ],
29 | "n": [ "in" ],
30 | "m": [ "ian" ],
31 | "vowelstart": "o"
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BYJRK/WpfTypingPractice/ed636046e91017e0b1c59ca89c61bdf18308a567/demo.png
--------------------------------------------------------------------------------