2010年11月24日 星期三

如何在C#中使用C++ Class DLL

如何在C#中使用Unmanaged dll 文章中曾介紹C#中使用C++ 函式庫(DLL)的方法, 不過此方法僅適用於C-Style export 出來的函式, 不行用在類別(Class)中的成員函式.


但其實很多API都是設計成C++的類別, 因此本文將介紹如何在C# 使用C++ Class的函式.

其步驟如下:

  • 設計一個C-Style function 介面, 透過指標存取物件的實體 – 由於C#中並沒有提供包裝C++ Class的方法, 所以我們必須將類別中public function重新包裝成C-Style function, 而一個物件的實體, 其實可以透過指標(*)作存取, C#中指標的使用也可透過IntPtr.
  • 利用C# PInvoke機制呼叫C-Style function– 作法可參考如何在C#中使用Unmanaged dll
  • 釋放unmanaged object記憶體 –由於C#的回收機制,不會自動回收unmanaged object記憶體, 所以我們必須透過Dispose/Finalize手動回收, 其作法可參考C#記憶體管理

範例如下:

原始C++類別程式碼
#ifdef CSHAPEAPI_EXPORTS
 #define  CSHAPE_API  __declspec(dllexport)
#else
 #define  CSHAPE_API  __declspec(dllimport)
#endif


class CSHAPE_API CShape
{

public:
 CShape(){};
 virtual ~CShape(){};

public:
 float GetRectangleArea(float w ,float h) { return w*h; };  
 float GetTriangleArea(float b ,float h) { return 0.5*b*h;};
};

另外設計一個C-Style function 介面, 包裝要使用的C++ 類別

CShapeBridge.h
#include "CShapeAPI.h"
#ifdef __cplusplus
extern "C" {
#endif

extern CSHAPE_API CShape* CreateInstance();
extern CSHAPE_API void DisposeInstance(CShape* pShapeInstance);

extern CSHAPE_API float GetRectangleArea(CShape* pShapeInstance,
                                        float w ,float h);  
extern CSHAPE_API float GetTriangleArea(CShape* pShapeInstance,
                                        float b ,float h);


#ifdef __cplusplus
}
#endif

CShapeBridge.cpp
#include "stdafx.h"
#include "CShapeBridge.h"

extern "C"  CSHAPE_API CShape* CreateInstance()
{
 return new CShape();
}

extern "C"  CSHAPE_API void DisposeInstance(CShape* pShapeInstance)
{
 if(pShapeInstance != NULL)
 {
  delete pShapeInstance;
  pShapeInstance = NULL;
 }
}

extern "C"  CSHAPE_API float GetRectangleArea(CShape* pShapeInstance, 
                                              float w ,float h)
{

 return pShapeInstance->GetRectangleArea(w, h);
}

extern "C"  CSHAPE_API float GetTriangleArea(CShape* pShapeInstance, 
                                             float b ,float h)
{
 return pShapeInstance->GetTriangleArea(b, h);
}

利用PInvoke機制呼叫C-Style function , 並釋放unmanaged object記憶體

ShapeWrapper.cs
using System;
using System.Runtime.InteropServices;

namespace ShapeTest
{
    public class ShapeWrapper : IDisposable
    {
        #region PInvokes
        [DllImport("CShapeAPI.dll")]
        private static extern IntPtr CreateInstance();

        [DllImport("CShapeAPI.dll")]
        private static extern void DisposeInstance(IntPtr pShapeInstance);

        [DllImport("CShapeAPI.dll")]
        private static extern float GetRectangleArea(IntPtr pShapeInstance,
                                                     float w, float h);  
       
        [DllImport("CShapeAPI.dll")]
        private static extern float GetTriangleArea(IntPtr pShapeInstance, 
                                                    float b, float h);
        #endregion

        #region Members
        // Variable to hold the C++ class's this pointer
        private IntPtr m_pNativeObject;  
        #endregion Members


        public ShapeWrapper()
        {
            // We have to Create an instance of 
            // this class through an exported function
            this.m_pNativeObject = CreateInstance();
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool bDisposing)
        {
            if (this.m_pNativeObject != IntPtr.Zero)
            {
                // Call the DLL Export to dispose this class
                DisposeInstance(this.m_pNativeObject);
                this.m_pNativeObject = IntPtr.Zero;
            }

            if (bDisposing)
            {
                // No need to call the finalizer since we've now cleaned
                // up the unmanaged memory
                GC.SuppressFinalize(this);
            }
        }

        // This finalizer is called when Garbage collection occurs, but only if
        // the IDisposable.Dispose method wasn't already called.
        ~ShapeWrapper()
        {
            Dispose(false);
        }


        #region Wrapper methods
        public float GetRectangleArea(float w, float h)
        {

            return GetRectangleArea(m_pNativeObject, w, h);
        }

        public float GetTriangleArea(float b, float h)
        {
            return GetTriangleArea(m_pNativeObject, b, h);
        }
        #endregion
    }
}


測試C#程式使用C++ Class DLL
using System;
namespace ShapeTest
{
    class Program
    {
        static void Main(string[] args)
        {
            ShapeWrapper shape = new ShapeWrapper();

            float w= 25.0f;
            float h= 10.0f;
            Console.WriteLine("Rectangle area = "+shape.GetRectangleArea(w, h));


            float b = 30.0f;
            float h1 = 20.0f;
            Console.WriteLine("Triangle area = "+ shape.GetTriangleArea(b, h1));

        }
    }
} 


測試結果:
Rectangle area = 250
Triangle area = 300

另外, MSDN也有提供使用CLR的方法, 請參考包裝原生類別以便讓 C# 使用

參考文章:

2010年11月11日 星期四

Background Process in WPF

在程式執行中, 有時後需要處理一些比較耗時的程序, 例如讀取或計算大量的資料, 因為這些動作會花比較多的時間, 而且會讓畫面停留不動. 所以, 通常需要一個Loading/Processing的畫面來提醒使用者.

Loading過程中, 如果要更新UI內容的話, 需要在背景工作, 否則會等Loading處理完畢後, 才會更新UI.

下面的範例中, 程式會顯示Loading過程中的次數和完成的百分比.
private void UpdateProcess()
 {
     for (int i = 1; i <= _numberOfTasks; i++)
     {
         labelNumber.Content = i +"/" + _numberOfTasks;
         float percentageDone = (i / (float)_numberOfTasks) * 100f;
         labelPercent.Content = (int)percentageDone+"%";
         Thread.Sleep(1);
     }
}


從執行結果可看出, 因為不是在背景中(background)更新UI內容, 所以畫面會停住不動, Loading完才會更新UI內容

 
[影片1] 不在背景工作中更新UI 


WPF中提供多個背景工作的方法, 下面逐一介紹

1. 使用Dispatcher

此方法最為簡單, 我們可將Dispatcher Priority 設成 DispatcherPriority.Background,即可在背景執行,而Dispatcher的相關用法可參考這篇文章WPF - Thread and Dispatcher
因此我們可將上面的程式碼, 改成為下面的程式:

private delegate void UpdateProcessDelegate(int index, int percent);
private void UpdateProcessAction(int index, int percent)
{
    labelNumber.Content = index + "/" + _numberOfTasks;
    labelPercent.Content = percent + "%";
 }

 private void UpdateProcess()
 {
     for (int i = 1; i <= _numberOfTasks; i++)
     {
         //labelNumber.Content = i +"/" + _numberOfTasks;
         float percentageDone = (i / (float)_numberOfTasks) * 100f;
         //labelPercent.Content = (int)percentageDone+"%";

         Dispatcher.Invoke(DispatcherPriority.Background,
                           new UpdateProcessDelegate(UpdateProcessAction),
                           i, 
                           (int)percentageDone);
         
         Thread.Sleep(1);
      }
 }

 
[影片2] 使用Dispatcher更新UI內容

 
2. 使用DispatcherFrame       
此作法類似Windows Form的DoEvents.  
DispatcherFrame 表示處理暫停之工作項目的迴圈.發送器會處理迴圈中的工作項目佇列. 此迴圈即稱為框架。通常是由應用程式以呼叫 Run 的方式來初始化初始迴圈. PushFrame 進入以 frame 參數表示的迴圈。對該迴圈的每次查看, Dispatcher 都會檢查 DispatcherFrame 類別上的 Continue 屬性,以判斷迴圈是否應繼續或停止.

程式碼如下:
public void DoEvents()
{
   DispatcherFrame frame = new DispatcherFrame();
   Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                                            new DispatcherOperationCallback(ExitFrame),
                                            frame);
   Dispatcher.PushFrame(frame);
}

private object ExitFrame(object f)
{
   ((DispatcherFrame)f).Continue = false;
   return null;
}

private void UpdateProcess()
{
   for (int i = 1; i <= _numberOfTasks; i++)
   {
       labelNumber.Content = i +"/" + _numberOfTasks;
       float percentageDone = (i / (float)_numberOfTasks) * 100f;
       labelPercent.Content = (int)percentageDone+"%";
       DoEvents();
       Thread.Sleep(1);
    }  
 }

BackgroundWorker可以在個別的執行緒上執行. BackgroundWoker常用的function如下:
程式碼如下:
private void btnShowText_Click(object sender, RoutedEventArgs e)
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.WorkerReportsProgress = true;
    worker.DoWork += new DoWorkEventHandler(DoWork);
    worker.ProgressChanged += new ProgressChangedEventHandler(DuringWork);
    
    // start the background work
    worker.RunWorkerAsync();
 }

 void DoWork(object sender, DoWorkEventArgs e)
 {
     BackgroundWorker worker = sender as BackgroundWorker;
     for (int i = 1; i <= _numberOfTasks; i++)
     {
         if (worker.CancellationPending)
         {
            e.Cancel = true;
            return;
          }
          Thread.Sleep(1);
          float percentageDone = (i / (float)_numberOfTasks) * 100f;
          worker.ReportProgress((int)percentageDone, i);
      }
  }
       
 void DuringWork(object sender, ProgressChangedEventArgs e)
 {
    labelNumber.Content = e.UserState + "/" + _numberOfTasks;
    labelPercent.Content = e.ProgressPercentage.ToString() + "%";
 }


參考文章:

2010年11月10日 星期三

WPF - Thread and Dispatcher

WPF 執行緒:
WPF應用程式中主要有兩個執行緒(Thread):Rendering ThreadUI Thread
  • Rendering Thread: 在背景中執行並協助UI Thread完成工作
  • UI Thread: 主要負責處理InputEvent與程式碼的部分
WPF中, 所有UI工作和事件都是在UI Thread上完成. 每個WPF程式至少有一個Dispatcher物件和一個UI Thread. 而大部分WPF物件的操作都是binding在UI Thread上,其他的Thread如果想要直接操作這些UI物件, 是不被允許的, 因為只有建立 DispatcherObject 的Thread才可以存取UI物件。例如, 要讓其他的Thread存取 LabelContent 屬性, 將會發生錯誤, 此範例如下所示:
設計一個WPF視窗, 當按下Button後, 讓Label顯示按Button的次數.

C#程式碼如下所示:
private static int _clickCount = 0;
private void btnShowText_Click(object sender, RoutedEventArgs e)
{
      _clickCount++;
       // Update Label Conten in another thread
       Thread updateThread = new Thread(UpdateText);
       updateThread.Start();
 }
 private void UpdateText()
 {
      labelClickCount.Content = "Click Count= " + _clickCount.ToString();
 }

[圖1]無法再另一個Thread中更新UI


Dispatcher用法:

Dispatcher可讓其他的ThreadWPF中的 UI Thread上執行程式碼. Dispatcher又被稱為WPF中的Message Pump,提供一個機制去發送工作項目給UI Thread進行處理.  
Dispatcher將工作項目轉給UI Thread時, 會讓UI Thread進入封鎖狀態(畫面鎖定, 因為要處理工作項目). 其他的Thread必須將工作委派給與 UI Thread相關聯的 Dispatcher. 這可以使用 Invoke BeginInvoke 完成.
  • Invoke 是同步呼叫,也就是說,除非 UI 執行緒實際完成委派的執行,否則不會返回. 
Dispatcher 會依優先權排列其佇列中項目的順序. 在將項目加入至 Dispatcher 佇列.

下面的程式碼, 將說明其解決方法:
C#程式碼如下所示:

private static int _clickCount = 0;

 // Declare a delegate type for updating UI
 private delegate void UpdateDelegate(int number, string str);
 private void btnShowText_Click(object sender, RoutedEventArgs e)
 {
     _clickCount++;
     Thread updateThread = new Thread(UpdateText);
      updateThread.Start();
 }

 private void UpdateText()
 {
      //labelClickCount.Content = "Click Count= " + _clickCount.ToString();
      // Places the delegate onto the UI Thread's Dispatcher
      Dispatcher.Invoke(new UpdateDelegate(UpdateClickCountAction),
                                          _clickCount, 
                                          "Click Count = ");
 }

  private void UpdateClickCountAction(int number, string str)
  {
       labelClickCount.Content = str + number.ToString();
  }

[圖2] 利用Dispatcher執行另一個Thread的function

參考文章: