時間過得真快,距離上次發隨筆又是一年多。作為上次發的我的第一個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>
界面設計好之后,開始寫邏輯代碼。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 }
碎覺。。