2014年11月25日 星期二

[軟體/Debug]於win7環境下提示視窗帶至前景的功能(SetForegroundWindow)偶發失敗

最近前台電腦環境轉換至win7,平常在winXP運作正常將提示視窗帶到前景的功能,會偶發失敗(壓視窗)的情況發生。對於醫師來說,這非常影響門診看診的"順暢性"。所以,非常認真的找了一下資料,不然電話那頭都會傳來關切的聲音(好累喔!)...
最後參考了CodeProject的這篇文章"How to bring window to top with SetForegroundWindow()",並將相關程式碼轉換成C#的語法來使用。因為仍有電腦環境的問題要考慮,所以並沒有整合到目前的程式碼,不過還是記錄一下好了!

[DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd);

[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();

[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();

[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr ProcessId);

[DllImport("user32.dll")]
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

public const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
public const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
public const uint SPIF_SENDWININICHANGE = 0x02;
public const uint SPIF_UPDATEINIFILE = 0x01;
[DllImport("user32.dll")]
public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref uint pvParam, uint fWinIni);

public const int ASFW_ANY = -1;
[DllImport("user32.dll")]
public static extern bool AllowSetForegroundWindow(int dwProcessId);

[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);

/// <summary>
/// How to bring window to top with SetForegroundWindow()
/// http://www.codeproject.com/Tips/76427/How-to-bring-window-to-top-with-SetForegroundWindo
/// </summary>
/// <param name="hWnd"></param>
private void SetForegroundWindowInternal(IntPtr hWnd)
{
 if(!IsWindow(hWnd)) return;

 //relation time of SetForegroundWindow lock
 uint lockTimeOut = 0;
 IntPtr hCurrWnd = GetForegroundWindow();
 uint dwThisTID = GetCurrentThreadId();
 uint dwCurrTID = GetWindowThreadProcessId(hCurrWnd, IntPtr.Zero);

 //we need to bypass some limitations from Microsoft :)
 if (dwThisTID != dwCurrTID)
 {
  AttachThreadInput(dwThisTID, dwCurrTID, true);

  SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, ref lockTimeOut, 0);
  SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, ref lockTimeOut, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE);

  AllowSetForegroundWindow(ASFW_ANY);
 }

 SetForegroundWindow(hWnd);

 if (dwThisTID != dwCurrTID)
 {
  SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0,ref lockTimeOut, SPIF_SENDWININICHANGE | SPIF_UPDATEINIFILE);
  AttachThreadInput(dwThisTID, dwCurrTID, false);
 }
}

參考資料
[1] 把視窗移到最前面
[2] How to bring window to top with SetForegroundWindow()
[3] Control in Focus in Other Processes
[4] PInvoke.net

2014年11月19日 星期三

[硬體/系統]設定單一登入(SSO)環境

1. 安裝ADAM(Active Directory Application Mode)於Windows XP

2. 程式方面~
引用:System.DirectoryServices.DirectorySearcher
string cn = "UserName";
string cnPassword = "UserPassword";

string path = "LDAP://ServerName:389/dc=DomainName,dc=com,dc=tw";
string username = "DomainName\\DomainAdmin"; 
string password = "DomainAdminPassword";

// Authentication flags.
// For non-secure connection, use LDAP port and
//  ADS_USE_SIGNING | ADS_USE_SEALING | ADS_SECURE_AUTHENTICATION
// For secure connection, use SSL port and
//  ADS_USE_SSL | ADS_SECURE_AUTHENTICATION
AuthenticationTypes authenticationTypes  = AuthenticationTypes.Signing | AuthenticationTypes.Sealing | AuthenticationTypes.Secure;

const long ADS_OPTION_PASSWORD_PORTNUMBER = 6;
const long ADS_OPTION_PASSWORD_METHOD = 7;
const int ADS_PASSWORD_ENCODE_REQUIRE_SSL = 0;
const int ADS_PASSWORD_ENCODE_CLEAR = 1;

/// <summary>
/// 新增 LDAP 使用者
/// </summary>
private void Add(string cn)
{
 try
 {
  // 定義 DirectoryEntry
  DirectoryEntry entry = new DirectoryEntry(path, username, password, authenticationTypes);

  // 檢查是否存在 LDAP 使用者
  DirectorySearcher searcher = new DirectorySearcher(entry);
  searcher.Filter = string.Format("(cn={0})", cn);
  SearchResult result = searcher.FindOne();

  // 新增 LDAP 使用者
  if (result == null)
  {
   DirectoryEntry user = entry.Children.Add(string.Format("cn={0},ou=Users", cn), "inetOrgPerson");
   //user.Properties["cn"].Value = "PK";
   user.Properties["displayName"].Value = &quot顯示名稱";
   user.Properties["department"].Value = "部門";
   user.Properties["departmentNumber"].Value = "部門代碼";
   user.Properties["givenname"].Value = "名";
   user.Properties["mail"].Value = "電子郵件";
   user.Properties["sn"].Value = "姓";
   user.Properties["uid"].Value = "帳號";
   user.Properties["userPrincipalName"].Value = "帳戶";
   user.CommitChanges();
   user.Close();
  }
 }
 catch (Exception ex)
 {
  throw new Exception(ex.Message);
 }
}

/// <summary>
/// 變更 LDAP 使用者密碼
/// </summary>
private void ChangePassword(string cn, string password)
{
 try
 {
  // 定義 DirectoryEntry
  DirectoryEntry entry = new DirectoryEntry(path, username, password, authenticationTypes);

  // 檢查是否存在 LDAP 使用者
  DirectorySearcher searcher = new DirectorySearcher(entry);
  searcher.Filter = string.Format("(cn={0})", cn);
  SearchResult result = searcher.FindOne();

  // 變更 LDAP 使用者密碼
  if (result != null)
  {
   DirectoryEntry user = result.GetDirectoryEntry();
   user.Invoke("SetOption", new object[] { ADS_OPTION_PASSWORD_PORTNUMBER, 389 });
   user.Invoke("SetOption", new object[] { ADS_OPTION_PASSWORD_METHOD, ADS_PASSWORD_ENCODE_CLEAR });
   user.Invoke("SetPassword", new Object[] { cnPassword });
   user.Properties["LockOutTime"].Value = 0;
   user.CommitChanges();
   user.Close();
  }
 }
 catch (Exception ex)
 {
  throw new Exception(ex.Message);
 }
}

/// <summary>
/// 刪除 LDAP 使用者
/// </summary>
private void Delete(string cn)
{
 try
 {
  // 定義 DirectoryEntry
  DirectoryEntry entry = new DirectoryEntry(path, username, password, authenticationTypes);

  // 檢查是否存在 LDAP 使用者
  DirectorySearcher searcher = new DirectorySearcher(entry);
  searcher.Filter = string.Format("(cn={0})", cn);
  SearchResult result = searcher.FindOne();

  // 刪除 LDAP 使用者
  if (result != null)
  {
   DirectoryEntry user = entry.Children.Find(string.Format("cn={0},ou=Users", cn), "inetOrgPerson");
   user.DeleteTree();
  }
 }
 catch (Exception ex)
 {
  throw new Exception(ex.Message);
 }
}

3. L7 Networks(上網認證)


 4. zimbra(電子郵件)


 5. moodle(數位學習) 1.9.12






參考資料
[1] Setting User Passwords
[2] LDAP Authentication

2014年11月11日 星期二

[硬體/系統]利用dd指令及JPerf(iperf)工具來量測XenServer的儲存及網路效能

很無奈的!我們在工作上並沒有太多的資源,所以我們的虛擬化環境是用免費的XenServer所架設,有時候遇到效能的問題,實在不容易找出可以努力改善的方向。但事情遇到了,還是要想辦法解決。在網路上看過幾份資料後,利用幾個簡單的指令及工具,建立目前環境的基準值。日後遇到效能瓶頸或是要升級硬體設備時,就有一個可以比較的基準。

儲存I/O效能工具~利用dd指令來量測
  • XenServer主機上
dd if=/dev/sda of=/dev/null bs=1M count=500 iflag=direct

  • 虛擬主機上
dd if=/dev/xvdb of=/dev/null bs=1M count=500 iflag=direct

網路I/O效能工具~利用JPerf(iperf)工具來量測

  • Server端
輸入"iperf.exe -s"執行伺服器模式。

Client端~
執行jperf.bat,並輸入Server端的ip,選擇輸出格式為MBytes,按下"Run IPerf!"

參考資料
[1] Xen and XenServer Storage Performance
[2] JPerf 2.0.2
[3] 使用Iperf測試網路效能
[4] XenServer Virtual Machine Performance Utility

2014年11月6日 星期四

健保e化抽審-3(國際條碼、Code 128)

我們原本使用的是 Code 39 的條碼,因為我們為了簡化進貨或盤點時的資料輸入(如:有效日期或批號等等...),再加上 Code 39 的資料越多則長度就越長,甚至會超出 Barcode Reader 的可掃瞄範圍,再加上很多的醫藥材都是使用 Code 128,所以我們導入了國際條碼。這個過程真的花了一番心力,現在我們將這項成果導入在文件的生成與辨識。

注意!以下這些 Code,已經忘記出處在那裡,若有人知道煩請告知,真是對不起原作者!

一、了解及準備 Code 128 字型檔
這兩篇 Blog 不錯,對於不了解什麼是 Barcode 的人,可以有初步的認識。
二、轉換 Barcode 資料及產生 Barcode 影像
Private Sub btnCode128_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCode128.Click

    Dim _data As String = "038000356216"

    ' Font Style
    Dim privateFonts As New System.Drawing.Text.PrivateFontCollection()
    privateFonts.AddFontFile("C:\WINDOWS\Fonts\3of9.ttf")
    privateFonts.AddFontFile("C:\WINDOWS\Fonts\code128.ttf")

    Dim font39 As New System.Drawing.Font(privateFonts.Families(0), 36)
    Dim font128 As New System.Drawing.Font(privateFonts.Families(1), 36)

    ' Code39:資料前後加上"*"做為啟始碼/停止碼即可
    Me.lblCode39.Font = font39
    Me.lblCode39.Text = BarcodeConverter39.StringToBarcode(_data)
    Me.lblCode39.AutoSize = True

    ' Code128:是由 啟始碼+資料碼+檢查碼+停止碼 所組合而成的條碼
    Me.lblCode128.Font = font128
    Me.lblCode128.Text = BarcodeConverter128.StringToBarcode(_data)

    ' Image Style
    Dim barcode As New BarcodeLib.Barcode()

    barcode.IncludeLabel = True
    barcode.Width = 300
    barcode.Height = 150
    barcode.Encode(BarcodeLib.TYPE.CODE128, _data)

    Dim ms As New MemoryStream()
    barcode.SaveImage(ms, BarcodeLib.SaveTypes.PNG)

    Me.PictureBox1.Image = System.Drawing.Image.FromStream(ms)

End Sub

三、取出 Barcode 資料
參考 GS1 General Specifications(Version 12),我們需要取得商品代號、有效日期及批號。
  • (01)商品代號~固定長度(數字形態,長度14)
  • (17)有效日期~固定長度(數字形態,長度6)
  • (10)批號~變動長度(數字形態,長度最多20)
private void btnGS1Barcode_Click(object sender, EventArgs e)
{
 string barcode;
 // 測試樣本
 //barcode = "0108888893102146";
 //barcode = "010888889310214617120315";
 barcode = "01088888931021461712031510W1040190";
 string Ai01; // (01)08888893102146
 string Ai17; // (17)120315     
 string Ai10; // (10)W1040190

 GS1BarcodeConvert(barcode, out Ai01, out Ai17, out Ai10);

 Application.DoEvents();
}

private void GS1BarcodeConvert(string pBarcode, out string pAi01, out string pAi17, out string pAi10)
{
 pAi01 = string.Empty;
 pAi17 = string.Empty;
 pAi10 = string.Empty;

 string AI;
 string DataContent = pBarcode;
 while (!string.IsNullOrEmpty(DataContent))
 {
  AI = DataContent.Substring(0, 2);

  if (AI == "01")
  {
   // Global Trade Item Number (GTIN)
   // N2 N14
   pAi01 = DataContent.Substring(2, 14);
   if (checkSum(pAi01.Remove(pAi01.Length - 1, 1), Int32.Parse(pAi01.Substring(pAi01.Length - 1, 1))))
   {
   }
   else
   {
    pAi17 = string.Empty;
   }
   DataContent = DataContent.Substring(16);
  }
  else if (AI == "17")
  {
   // Expiration Date (YYMMDD) 
   // N2 N6
   pAi17 = DataContent.Substring(2, 6);
   if (checkDate(pAi17) > DateTime.MinValue)
   {
   }
   else
   {
    pAi17 = string.Empty;
   }
   DataContent = DataContent.Substring(8);
  }
  else if (AI == "10")
  {
   // Batch or Lot Number 
   // N2 X..20
   pAi10 = DataContent.Length < 20 ? DataContent.Substring(2, DataContent.Length - 2) : DataContent.Substring(2, 20);
   DataContent = DataContent.Substring(pAi10.Length   2);
  }
 }

}

private Boolean checkSum(String pgtin, Int32 pchecksum)
{
 Boolean ret = false;
 Int32 glength = 0;
 Int32 total = 0;
 Int32 cSum = 0;
 Int32[] mutiply = { 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3 };
 glength = 17 - pgtin.Length;
 for (int i = 0; i < pgtin.Length; i  )
 {
  total = total   (Int32.Parse(pgtin[i].ToString()) * mutiply[i   glength]);
 }
 cSum = 10 - (total % 10);
 if (cSum == pchecksum)
 {
  ret = true;
 }
 return ret;

}

private DateTime checkDate(string pdate)
{
 DateTime ret = DateTime.MinValue;
 DateTime convertedDate = DateTime.MinValue;
 String dFormat = "yyMMdd";
 if (DateTime.TryParseExact(pdate, dFormat, System.Globalization.CultureInfo.CurrentCulture, System.Globalization.DateTimeStyles.None, out convertedDate))
 {
  ret = convertedDate;
 }
 return ret;
}

private void GS1DataConvert(string pBarcode, out string pAi01, out string pAi17, out string pAi10)
{
 pAi01 = string.Empty;
 pAi17 = string.Empty;
 pAi10 = string.Empty;

 String aiFull = "";
 String aiWCheckSum = "";
 String aiValue = "";
 Int32 aiCheckSum = 0;
 Int32 aiMinLength = 0;
 Int32 aiMaxLength = 0;
 int index = 0;
 if (pBarcode.Contains("01"))
 {
  index = pBarcode.IndexOf("01")   2;
  AII sai = getAiInfo("01");
  aiMinLength = sai.minLength;
  aiMaxLength = sai.maxLength;

  aiFull = pBarcode.Substring(index - 2, aiMaxLength   2);
  aiWCheckSum = pBarcode.Substring(index, aiMaxLength);
  aiValue = aiWCheckSum.Remove(aiWCheckSum.Length - 1, 1);
  aiCheckSum = Int32.Parse(aiWCheckSum.Substring(aiWCheckSum.Length - 1, 1));
  if (checkSum(aiValue, aiCheckSum))
  {
   pBarcode = pBarcode.Replace(aiFull, String.Empty);
   pAi01 = aiValue;
  }
 }
 if (pBarcode.Contains("17"))
 {
  index = pBarcode.IndexOf("17")   2;
  AII sai = getAiInfo("17");
  aiFull = pBarcode.Substring(index - 2, sai.minLength   2);
  aiValue = pBarcode.Substring(index, sai.minLength);
  if (checkDate(aiValue) > DateTime.MinValue)
  {
   pBarcode = pBarcode.Replace(aiFull, String.Empty);
   pAi17 = aiValue;
  }
 }
 if (pBarcode.Contains("10"))
 {
  index = pBarcode.IndexOf("10")   2;
  AII sai = getAiInfo("10");
  aiMinLength = sai.minLength;
  aiMaxLength = pBarcode.Length < sai.maxLength ? pBarcode.Length - 2 : sai.maxLength;
  aiFull = pBarcode.Substring(index - 2, aiMaxLength   2);
  aiValue = pBarcode.Substring(index, aiMaxLength);
  pAi10 = aiValue;
 }

}

public AII getAiInfo(String pAi)
{
 AII naii = new AII();
 if (pAi == "01")
 {
  naii.AICode = "01";
  naii.minLength = 14;
  naii.maxLength = 14;
  return naii;
 }
 if (pAi == "17")
 {
  naii.AICode = "17";
  naii.minLength = 6;
  naii.minLength = 6;
  return naii;
 }
 if (pAi == "10")
 {
  naii.AICode = "10";
  naii.minLength = 1;
  naii.maxLength = 20;
 }

 return naii;
}

public struct AII
{
 public String AICode;
 public Int32 minLength;
 public Int32 maxLength;
}
四、參考資料
[1] Free Barcode Font - Barcode String Builder
[2] Barcode Image Generation Library
[3] EAN128 or GS1-128 decode c#

2014年11月5日 星期三

健保e化抽審-2(WIA:Scan to PDF)

上次是利用列印的方法轉換成數位化檔案(PDF),這次則是使用掃瞄的方式來進行。利用WIA(Windows Image Acquisition)的掃瞄方式,可以將傳統的醫療單張掃瞄進來。
private void btnSacnToPDF_Click(object sender, EventArgs e)
{
 iTextSharp.text.Document document = new iTextSharp.text.Document(iTextSharp.text.PageSize.A4, 0, 0, 0, 0);
 try
 {
  List<string> devices = YGH.WIAScanner.GetDevices();
  if (devices.Count == 0)
  {
   throw new Exception("devices == 0");
  }

  List<System.Drawing.Image> images = YGH.WIAScanner.Scan(devices[0].Split('|')[0]);
  if (images.Count == 0)
  {
   throw new Exception("images == 0");
  }

  string fileName;
  // 方法1: 寫死暫存檔案名
  fileName = @"C:\temp\scan.pdf";
  // 方法2: 利用 System.IO.Path.GetTempPath() 及 Guid.NewGuid() 取得暫存檔案名
  //fileName = System.IO.Path.GetTempPath() + Guid.NewGuid().ToString() + ".pdf";
  // 方法3: 使用 System.IO.Path.GetTempFileName() 建立暫存檔案(副檔名為 .TMP)
  //fileName = System.IO.Path.GetTempFileName();

  PdfWriter.GetInstance(document, new FileStream(fileName, FileMode.Create));
  document.Open();

  foreach (System.Drawing.Image image in images)
  {
   iTextSharp.text.Image img = iTextSharp.text.Image.GetInstance(imageToByteArray(image));
   
   // 方法1: 跟 HP 內建掃瞄程式相比有明顯差異
   //img.ScaleToFit(document.PageSize.Width, document.PageSize.Height);

   // 方法2: 跟 HP 內建掃瞄程式相比差異較小
   img.SetAbsolutePosition(0, 0);
   img.ScalePercent(72.0F / img.DpiX * 100);

   // 方法3: 跟 HP 內建掃瞄程式相比有明顯差異
   //img.SetAbsolutePosition(0, 0);
   //img.ScaleAbsoluteWidth(document.PageSize.Width);
   //img.ScaleAbsoluteHeight(document.PageSize.Height);

   document.Add(img);
   document.NewPage();
  }
 }
 catch (Exception ex)
 {
  Console.WriteLine(ex.Message);
 }
 finally
 {
  document.Close();
 }
}

/// <summary>
/// C# Image to Byte Array and Byte Array to Image Converter Class
/// http://www.codeproject.com/Articles/15460/C-Image-to-Byte-Array-and-Byte-Array-to-Image-Conv
/// </summary>
/// <param name="imageIn"></param>
/// <returns></returns>
public byte[] imageToByteArray(System.Drawing.Image imageIn)
{
 MemoryStream ms = new MemoryStream();
 imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
 return ms.ToArray();
}

/// <summary>
/// C# Image to Byte Array and Byte Array to Image Converter Class
/// http://www.codeproject.com/Articles/15460/C-Image-to-Byte-Array-and-Byte-Array-to-Image-Conv
/// </summary>
/// <param name="byteArrayIn"></param>
/// <returns></returns>
public System.Drawing.Image byteArrayToImage(byte[] byteArrayIn)
{
 MemoryStream ms = new MemoryStream(byteArrayIn);
 System.Drawing.Image returnImage = System.Drawing.Image.FromStream(ms);
 return returnImage;
}
參考資料
[1] WIA Scanner in C# Windows Forms
[2] C# Image to Byte Array and Byte Array to Image Converter Class

2014年11月3日 星期一

健保e化抽審-1(PDFCreator:Print to PDF)

為了將傳統的報表轉換為數位的電子檔(PDF),我們將下戴並安裝PDFCreator,進而建立可以產生PDF檔的虛擬印表機。日後在列印報表時,我們可以使用此虛擬印表機來產生PDF檔,那我們就不用花太多的心力就可以簡單完成檔案數位的第一步。
PS~注意!若要整合在 Visual Studio 開發工具,1.3.2 及 1.4.0 版在中文檔名處理上有問題,會產生空白內容的 PDF 檔,目前測試 1.2.3 相容性較好。

1. 記得勾選"Expert setting",只安裝你所需要的軟體就好。


2. 取消安裝"PDFArchitect"


3. 記得看到這個畫面要按"cancel",不然又會幫你安裝一些軟體進來。


4. 參考資料
[1] PDFCreator
[2] 易普印之部落格 (e知識百科)~[免費] PDFCreator v1.9.5 正式版