跨過幾個坑,終于完成了我的第一個Xamarin Android App!
時間過得真快,距離上次發随筆又是一年多。作為上次發的我的第一個WP8.1應用總結的後繼,這次同樣的主要功能,改為實作安卓版APP。前幾個月巨硬收購Xamarin,把Xamarin內建到VS裡了,大大友善了我廣大.net碼農。由于年初脫了WP的坑,換了個安卓低端機,想着什麼時候裝Xamarin開發個App玩玩。
時間過得真快,距離上次發随筆又是一年多。作為上次發的我的第一個WP8.1應用總結的後繼,這次同樣的主要功能,改為實作安卓版APP。前幾個月巨硬收購Xamarin,把Xamarin內建到VS裡了,大大友善了我廣大.net碼農。由于年初脫了WP的坑,換了個安卓低端機,想着什麼時候裝Xamarin開發個App玩玩。
上個月筆記本100G的C槽莫名其妙快滿了,趁着重裝系統的機會,安裝了VS2015 with sp3,下載下傳開發Android App需要的各種東東。這裡要感謝【C#】VS2015開發環境的安裝和配置系列文章,2016-07-03更新的,已經算是最新的vs2015 with update3的安裝說明了。可惜看到這篇文章還是有點相見恨晚,文章裡的流程是先下載下傳安裝JDK和Android SDK等,最後安裝VS,我反過來做,浪費了一些時間。PS:對于使用Hyper-V的同學,可以使用VS自帶的安卓模拟器,省卻了下載下傳和安裝GOOGLE模拟器的一堆時間,據說GOOGLE模拟器還挺坑。。。
================================扯得太多,言歸正傳======================================
App項目用的是VS裡的Android Blank App,先上個圖讓大家看看我手機上的顯示效果,自己用就不需要那麼華麗麗了(關鍵是不會。。。)。

Android App使用的是顯示與邏輯分離的設計模式,雖然我基本是做Winform的,也是基本能看懂的。Resources\layout檔案夾裡放視圖檔案,而代碼邏輯檔案放在最外層,整個項目的結構如下圖:
這個App主界面使用的是GridLayout進行垂直布局,用法類似于HTML中的Table和WPF中的Grid,代碼如下:
1 <?xml version="1.0" encoding="utf-8"?>
2 <GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 android:orientation="horizontal"
6 android:paddingLeft="10dp"
7 android:paddingRight="10dp"
8 android:rowCount="6"
9 android:columnCount="2">
10 <TextView
11 android:id="@+id/tvMachineCode"
12 android:layout_width="wrap_content"
13 android:layout_marginRight="5dp"
14 android:textSize="20sp"
15 android:layout_marginTop="50dp"
16 android:text="機器碼" />
17 <EditText
18 android:id="@+id/txtMachineCode"
19 android:textSize="20sp"
20 android:maxLength ="20"
21 android:layout_marginTop="50dp"
22 android:layout_width="fill_parent" />
23 <TextView
24 android:id="@+id/tvActiviationCode"
25 android:layout_width="wrap_content"
26 android:layout_marginRight="5dp"
27 android:textSize="20sp"
28 android:layout_marginTop="30dp"
29 android:text="激活碼" />
30 <EditText
31 android:id="@+id/txtActiviationCode"
32 android:textSize="20sp"
33 android:layout_marginTop="30dp"
34 android:layout_width="fill_parent" />
35 <Button
36 android:id="@+id/btnGetActiviationCode"
37 android:layout_marginTop="30dp"
38 android:layout_columnSpan="2"
39 android:layout_width="match_parent"
40 android:text="擷取激活碼" />
41 <Button
42 android:id="@+id/btnScanQRCode"
43 android:layout_columnSpan="2"
44 android:layout_width="match_parent"
45 android:text="掃描二維碼" />
46 <Button
47 android:id="@+id/btnReadQRCode"
48 android:layout_columnSpan="2"
49 android:layout_width="match_parent"
50 android:text="讀取二維碼" />
51 <Button
52 android:id="@+id/btnCopy"
53 android:layout_columnSpan="2"
54 android:layout_width="match_parent"
55 android:text="複制激活碼" />
56 <Button
57 android:id="@+id/btnShare"
58 android:layout_columnSpan="2"
59 android:layout_width="match_parent"
60 android:text="發送激活碼" />
61 <Button
62 android:id="@+id/btnClear"
63 android:layout_columnSpan="2"
64 android:layout_width="match_parent"
65 android:text="清除" />
66 </GridLayout>
Main.axml
界面設計好之後,開始寫邏輯代碼。App預設是從MainActivity開始啟動(JAVA開發可以在Properties\AndroidManifest.xml中修改,有誰知道Xamarin裡是怎麼改的?)。開始實作第一個按鈕的功能,自我感覺還是比較容易的,基本可以直接複制粘貼我Winform裡的代碼,然而,我發現掉到第一個坑裡去了。先看看從Winform裡複制來的字元串取MD5的代碼,這個在VS自帶的模拟器中執行是正常的,得到的結果與Winform一緻,但安裝到手機裡得到的就不對了。
1 private string MD5(string str, bool clearsplitter = true, bool islower = true)
2 {
3 var md5 = MD5CryptoServiceProvider.Create();
4 var output = md5.ComputeHash(Encoding.Default.GetBytes(str));
5 StringBuilder strbvalue = new StringBuilder(BitConverter.ToString(output).Replace("-", string.Empty).Substring(8, 16));
6 if (!clearsplitter)
7 strbvalue.Insert(12, \'-\').Insert(8, \'-\').Insert(4, \'-\');
8 return islower ? strbvalue.ToString().ToLower() : strbvalue.ToString().ToUpper();
9 }
上網查了下,也問了下别人,電腦裡Encoding.Default用的編碼是GB2312,而手機裡可能是ASCII。由于不能修改之前的代碼,隻能改這個了,把Encoding.Default改成了Encoding.GetEncoding("gb2312"),結果出乎預料,竟然閃退了。。。又上網搜了下,需要引用Xamarin安裝自帶的I18N.CJK,總算是搞定了第一個按鈕。以下是【擷取激活碼】和【清除】的代碼:
private void Btngetactiviationcode_Click(object sender, EventArgs e)
{
string strerr = ValidateFormat(txtMachineCode.Text);
if (strerr != string.Empty)
{
var dlg = new AlertDialog.Builder(this).SetTitle("警告")
.SetMessage("輸入的機器碼格式不正确!\n" + strerr);
dlg.Show();
Btnclear_Click(this, null);
return;
}
txtActiviationCode.Text = GetActiveCode(txtMachineCode.Text);
}
private void Btnclear_Click(object sender, EventArgs e)
{
txtMachineCode.Text = txtActiviationCode.Text = string.Empty;
}
private string GetActiveCode(string machinecode)
{
string guid = "cd89e66c-b897-4ed8-a19f-ef5a30846f0a";
return MD5(machinecode + MD5(guid, false, false), false, false);
}
private string MD5(string str, bool clearsplitter = true, bool islower = true)
{
var md5 = MD5CryptoServiceProvider.Create();
var output = md5.ComputeHash(Encoding.GetEncoding("gb2312").GetBytes(str));
StringBuilder strbvalue = new StringBuilder(BitConverter.ToString(output).Replace("-", string.Empty).Substring(8, 16));
if (!clearsplitter)
strbvalue.Insert(12, \'-\').Insert(8, \'-\').Insert(4, \'-\');
return islower ? strbvalue.ToString().ToLower() : strbvalue.ToString().ToUpper();
}
【擷取激活碼】和【清除】
這個App的主要便利用途就是能夠掃描和識别二維碼,上網搜了下,使用ZXing庫會比較簡單,它有個.net移動開發版本叫ZXing.Net.Mobile,可以使用Nuget直接下載下傳添加引用,由于它依賴于Xamarin.Android.Support.v4,是以也要一起下載下傳安裝。直接按照ZXing.Net.Mobile官網上的掃描二維碼示例代碼,就做好了最簡單的二維碼掃描功能。注意:要在OnCreate方法裡先初始化一下:
1 MobileBarcodeScanner.Initialize(Application);
1 private async void Btnscanqrcode_Click(object sender, EventArgs e)
2 {
3 var scanner = new ZXing.Mobile.MobileBarcodeScanner();
4 var result = await scanner.Scan();
5 if (result == null)
6 return;
7 txtMachineCode.Text = result.Text.Trim();
8 Btngetactiviationcode_Click(this, null);
9 }
完成掃描二維碼的功能,頓時信心大增,以為識别圖檔中的二維碼也很簡單,結果發現又掉第二個坑裡去了。原來,ZXing.Net.Mobile裡沒有現成簡單的識别二維碼的方法,隻查到可以用IBarcodeReader.Decode()方法來識别,然而它第一個參數byte[] rawRGB是個什麼鬼?為毛不能提供一個Bitmap讓我爽一下?!去網上搜JAVA版的都是傳遞Bitmap對象,再去看了下ZXing.Net.Mobile的源碼,竟然是有些項目類型是Bitmap對象,有些是byte[]。沒時間深究,我還是自己來弄個byte[]吧。
印象中看到過一篇教程裡介紹過這個方法,說rawRGB參數指的是每個像素點的RGB值數組,而不是圖像檔案的二進制數組,這就要讀取圖像中的所有點的顔色值到數組裡裡再傳遞了。
1 private void Btnreadqrcode_Click(object sender, EventArgs e)
2 {
3 Intent = new Intent();
4 //從檔案浏覽器和相冊等選擇圖像檔案
5 Intent.SetType("image/*");
6 Intent.SetAction(Intent.ActionGetContent);
7 StartActivityForResult(Intent, 1);
8 }
9
10 protected override void OnActivityResult(int requestCode, [GeneratedEnum] Android.App.Result resultCode, Intent data)
11 {
12 base.OnActivityResult(requestCode, resultCode, data);
13 if(requestCode == 1 && resultCode == Android.App.Result.Ok && data != null)
14 {
15 // create a barcode reader instance
16 IBarcodeReader reader = new BarcodeReader();
17 // load a bitmap
18 int width = 0, height = 0;
19 //像素顔色值清單(注意:一個像素的每個顔色值都是一個清單中單獨的元素,
20 //後面将會把像素顔色值轉換成ARGB32格式的顔色,每個像素顔色值就有4個元素加入到清單中)
21 List<byte> pixelbytelist = new List<byte>();
22 try
23 {
24 //根據選擇的檔案路徑生成Bitmap對象
25 using (Bitmap bmp = Android.Provider.MediaStore.Images.Media.GetBitmap(ContentResolver, data.Data))
26 {
27 width = bmp.Width; //圖像寬度
28 height = bmp.Height; //圖像高度
29 // detect and decode the barcode inside the bitmap
30 bmp.LockPixels();
31 int[] pixels = new int[width * height];
32 //一次性讀取所有像素的顔色值(一個整數)到pixels
33 bmp.GetPixels(pixels, 0, width, 0, 0, width, height);
34 bmp.UnlockPixels();
35 for (int i = 0; i < pixels.Length; i++)
36 {
37 int p = pixels[i]; //取出一個像素顔色值
38 //将像素顔色值中的alpha顔色(透明度)添加到清單
39 pixelbytelist.Add((byte)Color.GetAlphaComponent(p));
40 //将像素顔色值中的紅色添加到清單
41 pixelbytelist.Add((byte)Color.GetRedComponent(p));
42 //将像素顔色值中的綠色添加到清單
43 pixelbytelist.Add((byte)Color.GetGreenComponent(p));
44 //将像素顔色值中的藍色添加到清單
45 pixelbytelist.Add((byte)Color.GetBlueComponent(p));
46 }
47 }
48 //識别
49 var result = reader.Decode(pixelbytelist.ToArray(), width, height, RGBLuminanceSource.BitmapFormat.ARGB32);
50 if (result != null)
51 {
52 txtMachineCode.Text = result.Text.Trim();
53 Btngetactiviationcode_Click(this, null);
54 }
55 else
56 Toast.MakeText(this, "未能識别到二維碼!", ToastLength.Short).Show();
57 }
58 catch (Exception ex)
59 {
60 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
61 .SetMessage("擷取圖像時發生錯誤!\n" + ex.ToString());
62 dlg.Show();
63 }
64 }
65 }
上面就完成了識别二維碼的功能,不過上面紅色文字那裡又出現個隻在手機上出現的詭異問題,識别出來的二維碼後面會多出一個不可見的字元,它會影響EditText中Text的長度,但不影響Text的值,可以被删除,删除前後計算出的激活碼是相同的。沒有去看源碼,不知道怎麼産生的,有人知道嗎?
後面的複制激活碼和發送激活碼比較簡單,都是直接找的網上的代碼,調用系統功能來做。
1 private void Btncopy_Click(object sender, EventArgs e)
2 {
3 ClipboardManager clip = (ClipboardManager)GetSystemService(ClipboardService);
4 StringBuilder strbcontent = new StringBuilder();
5 strbcontent.AppendLine("機器碼:" + txtMachineCode.Text)
6 .AppendLine("激活碼:" + txtActiviationCode.Text);
7 ClipData clipdata = ClipData.NewPlainText("激活碼", strbcontent.ToString());
8 clip.PrimaryClip = clipdata;
9 Toast.MakeText(this, "激活碼已複制到剪貼闆", ToastLength.Short).Show();
10 }
11
12 private void Btnshare_Click(object sender, EventArgs e)
13 {
14 if (string.IsNullOrWhiteSpace(txtActiviationCode.Text))
15 {
16 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
17 .SetMessage("請先擷取激活碼!");
18 dlg.Show();
19 return;
20 }
21 string strerr = ValidateFormat(txtMachineCode.Text);
22 if (strerr != string.Empty)
23 {
24 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
25 .SetMessage("輸入的機器碼格式不正确!\n" + strerr);
26 dlg.Show();
27 return;
28 }
29 Intent intent = new Intent(Intent.ActionSend);
30 intent.SetType("text/plain");//所有可以分享文本的app
31 StringBuilder strbcontent = new StringBuilder();
32 strbcontent.AppendLine("機器碼:" + txtMachineCode.Text)
33 .AppendLine("激活碼:" + txtActiviationCode.Text);
34 intent.PutExtra(Intent.ExtraText, strbcontent.ToString());
35 StartActivity(Intent.CreateChooser(intent, "發送激活碼"));
36 }
37
38 private string ValidateFormat(string str)
39 {
40 if(str.Length<19)
41 return "輸入的格式不正确";
42 if (str.Length != 19)
43 str = str.Substring(0, 19);
44 string[] strs = str.Split(\'-\');
45 if (strs.Length != 4)
46 return "不能分隔為4組";
47 foreach (string s in strs)
48 {
49 if (s.Length != 4)
50 return s + "的長度不是4";
51 if (!System.Text.RegularExpressions.Regex.IsMatch(s, "^[A-F0-9]{4}$"))
52 return s + "的格式不正确";
53 }
54 return string.Empty;
55 }
【複制激活碼】和【發送激活碼】
斷斷續續寫了幾個晚上,終于寫完這篇随筆了。在眼睛徹底睜不開之前趕緊貼上完整代碼。
1 using System;
2 using Android.App;
3 using Android.Content;
4 using Android.Runtime;
5 using Android.Views;
6 using Android.Widget;
7 using Android.OS;
8 using System.Text;
9 using System.Security.Cryptography;
10 using ZXing.Mobile;
11 using Android.Graphics;
12 using ZXing;
13 using Android.Database;
14 using System.Collections.Generic;
15
16 namespace FMSKeygen_Android
17 {
18 [Activity(Label = "流程管理系統注冊機", MainLauncher = true, Icon = "@drawable/icon")]
19 public class MainActivity : Activity
20 {
21 private EditText txtMachineCode = null;
22 private EditText txtActiviationCode = null;
23
24 protected override void OnCreate(Bundle bundle)
25 {
26 base.OnCreate(bundle);
27
28 // Set our view from the "main" layout resource
29 SetContentView(Resource.Layout.Main);
30
31 // 初始化二維碼掃描器,後面要用到
32 MobileBarcodeScanner.Initialize(Application);
33
34 txtMachineCode = FindViewById<EditText>(Resource.Id.txtMachineCode);
35 //設定自動轉換小寫字母為大寫
36 txtMachineCode.SetFilters(new Android.Text.IInputFilter[] { new Android.Text.InputFilterAllCaps() });
37 txtActiviationCode = FindViewById<EditText>(Resource.Id.txtActiviationCode);
38 //取消對驗證碼文本框的所有按鍵監聽
39 txtActiviationCode.KeyListener = null;
40 Button btnclear = FindViewById<Button>(Resource.Id.btnClear);
41 btnclear.Click += Btnclear_Click;
42 Button btngetactiviationcode = FindViewById<Button>(Resource.Id.btnGetActiviationCode);
43 btngetactiviationcode.Click += Btngetactiviationcode_Click;
44 Button btnscanqrcode = FindViewById<Button>(Resource.Id.btnScanQRCode);
45 btnscanqrcode.Click += Btnscanqrcode_Click;
46 Button btncopy = FindViewById<Button>(Resource.Id.btnCopy);
47 btncopy.Click += Btncopy_Click;
48 Button btnreadqrcode = FindViewById<Button>(Resource.Id.btnReadQRCode);
49 btnreadqrcode.Click += Btnreadqrcode_Click;
50 Button btnshare = FindViewById<Button>(Resource.Id.btnShare);
51 btnshare.Click += Btnshare_Click;
52 }
53
54
55 private void Btnshare_Click(object sender, EventArgs e)
56 {
57 if (string.IsNullOrWhiteSpace(txtActiviationCode.Text))
58 {
59 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
60 .SetMessage("請先擷取激活碼!");
61 dlg.Show();
62 return;
63 }
64 string strerr = ValidateFormat(txtMachineCode.Text);
65 if (strerr != string.Empty)
66 {
67 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
68 .SetMessage("輸入的機器碼格式不正确!\n" + strerr);
69 dlg.Show();
70 return;
71 }
72 Intent intent = new Intent(Intent.ActionSend);
73 intent.SetType("text/plain");//所有可以分享文本的app
74 StringBuilder strbcontent = new StringBuilder();
75 strbcontent.AppendLine("機器碼:" + txtMachineCode.Text)
76 .AppendLine("激活碼:" + txtActiviationCode.Text);
77 intent.PutExtra(Intent.ExtraText, strbcontent.ToString());
78 StartActivity(Intent.CreateChooser(intent, "發送激活碼"));
79 }
80
81 private string ValidateFormat(string str)
82 {
83 if(str.Length<19)
84 return "輸入的格式不正确";
85 if (str.Length != 19)
86 str = str.Substring(0, 19);
87 string[] strs = str.Split(\'-\');
88 if (strs.Length != 4)
89 return "不能分隔為4組";
90 foreach (string s in strs)
91 {
92 if (s.Length != 4)
93 return s + "的長度不是4";
94 if (!System.Text.RegularExpressions.Regex.IsMatch(s, "^[A-F0-9]{4}$"))
95 return s + "的格式不正确";
96 }
97 return string.Empty;
98 }
99
100 private void Btnreadqrcode_Click(object sender, EventArgs e)
101 {
102 Intent = new Intent();
103 //從檔案浏覽器和相冊等選擇圖像檔案
104 Intent.SetType("image/*");
105 Intent.SetAction(Intent.ActionGetContent);
106 StartActivityForResult(Intent, 1);
107 }
108
109 protected override void OnActivityResult(int requestCode, [GeneratedEnum] Android.App.Result resultCode, Intent data)
110 {
111 base.OnActivityResult(requestCode, resultCode, data);
112 if(requestCode == 1 && resultCode == Android.App.Result.Ok && data != null)
113 {
114 // create a barcode reader instance
115 IBarcodeReader reader = new BarcodeReader();
116 // load a bitmap
117 int width = 0, height = 0;
118 //像素顔色值清單(注意:一個像素的每個顔色值都是一個清單中單獨的元素,
119 //後面将會把像素顔色值轉換成ARGB32格式的顔色,每個像素顔色值就有4個元素加入到清單中)
120 List<byte> pixelbytelist = new List<byte>();
121 try
122 {
123 //根據選擇的檔案路徑生成Bitmap對象
124 using (Bitmap bmp = Android.Provider.MediaStore.Images.Media.GetBitmap(ContentResolver, data.Data))
125 {
126 width = bmp.Width; //圖像寬度
127 height = bmp.Height; //圖像高度
128 // detect and decode the barcode inside the bitmap
129 bmp.LockPixels();
130 int[] pixels = new int[width * height];
131 //一次性讀取所有像素的顔色值(一個整數)到pixels
132 bmp.GetPixels(pixels, 0, width, 0, 0, width, height);
133 bmp.UnlockPixels();
134 for (int i = 0; i < pixels.Length; i++)
135 {
136 int p = pixels[i]; //取出一個像素顔色值
137 //将像素顔色值中的alpha顔色(透明度)添加到清單
138 pixelbytelist.Add((byte)Color.GetAlphaComponent(p));
139 //将像素顔色值中的紅色添加到清單
140 pixelbytelist.Add((byte)Color.GetRedComponent(p));
141 //将像素顔色值中的綠色添加到清單
142 pixelbytelist.Add((byte)Color.GetGreenComponent(p));
143 //将像素顔色值中的藍色添加到清單
144 pixelbytelist.Add((byte)Color.GetBlueComponent(p));
145 }
146 }
147 //識别
148 var result = reader.Decode(pixelbytelist.ToArray(), width, height, RGBLuminanceSource.BitmapFormat.ARGB32);
149 if (result != null)
150 {
151 txtMachineCode.Text = result.Text.Trim();
152 Btngetactiviationcode_Click(this, null);
153 }
154 else
155 Toast.MakeText(this, "未能識别到二維碼!", ToastLength.Short).Show();
156 }
157 catch (Exception ex)
158 {
159 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
160 .SetMessage("擷取圖像時發生錯誤!\n" + ex.ToString());
161 dlg.Show();
162 }
163 }
164 }
165
166 private void Btncopy_Click(object sender, EventArgs e)
167 {
168 ClipboardManager clip = (ClipboardManager)GetSystemService(ClipboardService);
169 StringBuilder strbcontent = new StringBuilder();
170 strbcontent.AppendLine("機器碼:" + txtMachineCode.Text)
171 .AppendLine("激活碼:" + txtActiviationCode.Text);
172 ClipData clipdata = ClipData.NewPlainText("激活碼", strbcontent.ToString());
173 clip.PrimaryClip = clipdata;
174 Toast.MakeText(this, "激活碼已複制到剪貼闆", ToastLength.Short).Show();
175 }
176
177 private async void Btnscanqrcode_Click(object sender, EventArgs e)
178 {
179 var scanner = new ZXing.Mobile.MobileBarcodeScanner();
180 var result = await scanner.Scan();
181 if (result == null)
182 return;
183 txtMachineCode.Text = result.Text.Trim();
184 Btngetactiviationcode_Click(this, null);
185 }
186
187 private void Btngetactiviationcode_Click(object sender, EventArgs e)
188 {
189 string strerr = ValidateFormat(txtMachineCode.Text);
190 if (strerr != string.Empty)
191 {
192 var dlg = new AlertDialog.Builder(this).SetTitle("警告")
193 .SetMessage("輸入的機器碼格式不正确!\n" + strerr);
194 dlg.Show();
195 Btnclear_Click(this, null);
196 return;
197 }
198 txtActiviationCode.Text = GetActiveCode(txtMachineCode.Text);
199 }
200
201 private void Btnclear_Click(object sender, EventArgs e)
202 {
203 txtMachineCode.Text = txtActiviationCode.Text = string.Empty;
204 }
205
206 private string GetActiveCode(string machinecode)
207 {
208 string guid = "cd89e66c-b897-4ed8-a19f-ef5a30846f0a";
209 return MD5(machinecode + MD5(guid, false, false), false, false);
210 }
211
212 private string MD5(string str, bool clearsplitter = true, bool islower = true)
213 {
214 var md5 = MD5CryptoServiceProvider.Create();
215 var output = md5.ComputeHash(Encoding.GetEncoding("gb2312").GetBytes(str));
216 StringBuilder strbvalue = new StringBuilder(BitConverter.ToString(output).Replace("-", string.Empty).Substring(8, 16));
217 if (!clearsplitter)
218 strbvalue.Insert(12, \'-\').Insert(8, \'-\').Insert(4, \'-\');
219 return islower ? strbvalue.ToString().ToLower() : strbvalue.ToString().ToUpper();
220 }
221 }
222 }
完整代碼
碎覺。。