C#中調用Windows API的要點

 http://blog.csdn.net/jingshuaizh/article/details/3862019

API(Application Programming Interface),我想大家不會陌生,它是我們Windows編程的常客,雖然基於.Net平臺的C#有了強大的類庫,但是,我們還是不能否認API在Windows編程中的重要性。大多數的編程語言都支持API編程,而.Net平臺中的MFC(Microsoft Foundation Class Library)構架本身就封裝了大部分的API。 

 

  做爲程序員,我們需要了解API從字面上瞭解便是編程接口,因此,做爲開發者,需要了解的只是API的使用方法。 

 

  API根據操作系統、處理器及功能性的不同而擁有很多不同的類型。   操作系統特用的API: 

 

  每種操作系統都有許多通用的API以及一些特用的API,這些特用的API只能在當前操作系統中執行。 

 

  例如: 

 

  Windows NT 支持 MS-DOS, Win16, Win32, POSIX (Portable Operating System Interface), OS/2 console API; 而 Windows 95 支持 MS-DOS, Win16 以及 Win32 APIs. 

 

  Win16 & Win32 API: 

 

  Win16是爲十六位處理器開發的,早期的操作系統均支持。 

 

  Win32則是爲32位處理器開發。它可移植性強,被大部分的處理器所支持。 

 

  Win32 API在庫名後有一個”32”後綴。比如KERNEL32,USER32等。 

 

  所有API在下面3個庫中得以運行: 

 

 

  Kernel 

  User 

  GDI

 

  1. KERNEL 

 

  他的庫名爲 KERNEL32.DLL, 他主要用於產生與操作系統之間的關聯: 

 

  程序加載 

  上下文選擇. 

  文件輸入輸出. 

  內存管理. 

  例如: GlobalMemoryStatus 函數就包括當前物理內存及虛擬內存的使用信息。 

 

  2. USER 

 

  這個類庫在Win32中名叫 USER32.DLL。 

 

  它允許管理全部的用戶接口,比如: 

 

  窗口 

  菜單 

  對話框 

  圖標等., 

  例如: DrawIcon 函數將在指定的設備關聯上“畫”出圖標或者鼠標。 

 

  3. GDI (Graphical Device Interface) 

 

  它在Win32中的庫名爲:GDI32.dll,它是圖形輸出庫。使用GDI Windows“畫”出窗口、菜單以及對話框等: 

 

  它能創建圖形輸出. 

  它也能保存圖形文件. 

  例如: CreateBitmap 函數就能通過指定的長、寬、顏色創建一個位圖。

 

 

 

 

 

  C# 中操作API: 

 

  作爲初學者來說,在C#中使用API確是一件令人頭疼的問題。在使用API之間你必須知道如何在C#中使用結構、類型轉換、安全/不安全代碼,可控/不可控代碼等許多知識。 

 

  一切從簡單開始,複雜的大家一時不能接受。我們就從實現一個簡單的MessageBox開始。首先打開VS.Net ,創建一個新的C#工程,並添加一個Button按鈕。當這個按鈕被點擊,則顯示一個MessageBox對話框。 

 

  即然我們需要引用外來庫,所以必須導入一個Namespace: 

 

 

  using System.Runtime.InteropServices;

 

  接着添加下面的代碼來聲明一個API:

 

  [DllImport("User32.dll")] 

 

  public static extern int MessageBox(int h, string m, string c, int type);

 

  此處DllImport屬性被用來從不可控代碼中調用一方法。”User32.dll”則設定了類庫名。DllImport屬性指定dll的位置,這個dll中包括調用的外部方法。Static修飾符則聲明一個靜態元素,而這個元素屬於類型本身而不是上面指定的對象。extern則表示這個方法將在工程外部執行,使用DllImport導入的方法必須使用extern修飾符。 

 

  MessageBox 則是函數名,擁有4個參數,其返回值爲數字。 

 

  大多數的API都能傳遞並返回值。 

 

  添中Click點擊事件代碼:

 

  protected void button1_Click(object sender, System.EventArgs e) 

 

  { 

 

      MessageBox (0,"API Message Box","API Demo",0); 

 

  }

 

 

  編譯並運行這個程序,當你點擊按鈕後,你將會看到對話框,這便是你使用的API函數。 

 

  使用結構體 

 

  操作帶有結構體的API比使用簡單的API要複雜的多。但是一旦你掌握了API的過程,那個整個API世界將在你的掌握之中。 

 

  下面的例子中我們將使用GetSystemInfo API 來獲取整個系統的信息。 

 

  第一步還是打開C#建立一個Form工程,同樣的添中一個Button按鈕,在代碼窗中輸入下面的代碼,導入Namespace:

 

  using System.Runtime.InteropServices;

 

  聲明一個結構體,它將做爲GetSystemInfo的一個參數:

 

 

 

  [StructLayout(LayoutKind.Sequential)] 

 

  public struct SYSTEM_INFO { 

 

      public uint dwOemId; 

 

      public uint dwPageSize; 

 

      public uint lpMinimumApplicationAddress; 

 

      public uint lpMaximumApplicationAddress; 

 

      public uint dwActiveProcessorMask; 

 

      public uint dwNumberOfProcessors; 

 

      public uint dwProcessorType; 

 

      public uint dwAllocationGranularity; 

 

      public uint dwProcessorLevel; 

 

      public uint dwProcessorRevision; 

 

  }

 

  聲明API函數:

 

 

 

  [DllImport("kernel32")] 

 

  static extern void GetSystemInfo(ref SYSTEM_INFO pSI);

 

 

  添加下面的代碼至按鈕的點擊事件處理中: 

 

  首先創建一個SYSTEM_INFO結構體,並將其傳遞給GetSystemInfo函數。

 

 

 

  protected void button1_Click (object sender, System.EventArgs e) 

 

  { 

 

      try 

 

      { 

 

          SYSTEM_INFO pSI = new SYSTEM_INFO(); 

 

          GetSystemInfo(ref pSI); 

 

          // 

 

          // 

 

          //

 

  一旦你接收到返回的結構體,那麼就可以以返回的參數來執行操作了。

 

 

 

  e.g.listBox1.InsertItem (0,pSI.dwActiveProcessorMask.ToString());: 

 

          // 

 

          // 

 

          // 

 

     } 

 

     catch(Exception er) 

 

     { 

 

          MessageBox.Show (er.Message); 

 

     } 

 

  }

 

 

 

 

 

 

view plain

調用API全部代碼   

  

  //Created By Ajit Mungale   

  

  //程序補充 飛刀   

  

  namespace UsingAPI   

  

  {   

  

  using System;   

  

  using System.Drawing;   

  

  using System.Collections;   

  

  using System.ComponentModel;   

  

  using System.WinForms;   

  

  using System.Data;   

  

  using System.Runtime.InteropServices;   

  

  //Struct 收集系統信息   

  

  [StructLayout(LayoutKind.Sequential)]   

  

  public struct SYSTEM_INFO {   

  

        public uint dwOemId;   

  

        public uint dwPageSize;   

  

        public uint lpMinimumApplicationAddress;   

  

        public uint lpMaximumApplicationAddress;   

  

        public uint dwActiveProcessorMask;   

  

        public uint dwNumberOfProcessors;   

  

        public uint dwProcessorType;   

  

        public uint dwAllocationGranularity;   

  

        public uint dwProcessorLevel;   

  

        public uint dwProcessorRevision;   

  

    }   

  

  //struct 收集內存情況   

  

  [StructLayout(LayoutKind.Sequential)]   

  

  public struct MEMORYSTATUS   

  

  {   

  

       public uint dwLength;   

  

       public uint dwMemoryLoad;   

  

       public uint dwTotalPhys;   

  

       public uint dwAvailPhys;   

  

       public uint dwTotalPageFile;   

  

       public uint dwAvailPageFile;   

  

       public uint dwTotalVirtual;   

  

       public uint dwAvailVirtual;   

  

  }   

  

  public class Form1 : System.WinForms.Form   

  

  {   

  

    private System.ComponentModel.Container components;   

  

    private System.WinForms.MenuItem menuAbout;   

  

    private System.WinForms.MainMenu mainMenu1;   

  

    private System.WinForms.ListBox listBox1;   

  

    private System.WinForms.Button button1;   

  

  //獲取系統信息   

  

    [DllImport("kernel32")]   

  

    static extern void GetSystemInfo(ref SYSTEM_INFO pSI);   

  

    //獲取內存信息   

  

    [DllImport("kernel32")]   

  

    static extern void GlobalMemoryStatus(ref MEMORYSTATUS buf);   

  

    //處理器類型   

  

    public const int PROCESSOR_INTEL_386 = 386;   

  

    public const int PROCESSOR_INTEL_486 = 486;   

  

    public const int PROCESSOR_INTEL_PENTIUM = 586;   

  

    public const int PROCESSOR_MIPS_R4000 = 4000;   

  

    public const int PROCESSOR_ALPHA_21064 = 21064;   

  

    public Form1()   

  

    {   

  

      InitializeComponent();   

  

    }   

  

    public override void Dispose()   

  

    {   

  

      base.Dispose();   

  

      components.Dispose();   

  

    }   

  

    private void InitializeComponent()   

  

     {   

  

       this.components = new System.ComponentModel.Container ();   

  

       this.mainMenu1 = new System.WinForms.MainMenu ();   

  

       this.button1 = new System.WinForms.Button ();   

  

       this.listBox1 = new System.WinForms.ListBox ();   

  

       this.menuAbout = new System.WinForms.MenuItem ();   

  

       mainMenu1.MenuItems.All = new System.WinForms.MenuItem[1] {this.menuAbout};   

  

       button1.Location = new System.Drawing.Point (148, 168);   

  

       button1.Size = new System.Drawing.Size (112, 32);   

  

       button1.TabIndex = 0;   

  

       button1.Text = "&Get Info";   

  

       button1.Click += new System.EventHandler (this.button1_Click);   

  

       listBox1.Location = new System.Drawing.Point (20, 8);   

  

       listBox1.Size = new System.Drawing.Size (368, 147);   

  

       listBox1.TabIndex = 1;   

  

       menuAbout.Text = "&About";   

  

       menuAbout.Index = 0;   

  

       menuAbout.Click += new System.EventHandler (this.menuAbout_Click);   

  

       this.Text = "System Information - Using API";   

  

       this.MaximizeBox = false;   

  

       this.AutoScaleBaseSize = new System.Drawing.Size (5, 13);   

  

       this.MinimizeBox = false;   

  

       this.Menu = this.mainMenu1;   

  

       this.ClientSize = new System.Drawing.Size (408, 213);   

  

       this.Controls.Add (this.listBox1);   

  

       this.Controls.Add (this.button1);   

  

    }   

  

    protected void menuAbout_Click (object sender, System.EventArgs e)   

  

    {   

  

       Form abt=new about() ;   

  

       abt.ShowDialog();   

  

    }   

  

    protected void button1_Click (object sender, System.EventArgs e)   

  

    {   

  

       try   

  

       {   

  

          SYSTEM_INFO pSI = new SYSTEM_INFO();   

  

          GetSystemInfo(ref pSI);   

  

          string CPUType;   

  

          switch (pSI.dwProcessorType)   

  

          {   

  

            case PROCESSOR_INTEL_386 :   

  

               CPUType= "Intel 386";   

  

               break;   

  

            case PROCESSOR_INTEL_486 :   

  

               CPUType = "Intel 486" ;   

  

              break;   

  

            case PROCESSOR_INTEL_PENTIUM :   

  

              CPUType = "Intel Pentium";   

  

              break;   

  

            case PROCESSOR_MIPS_R4000 :   

  

              CPUType = "MIPS R4000";   

  

              break;   

  

            case PROCESSOR_ALPHA_21064 :   

  

              CPUType = "DEC Alpha 21064";   

  

              break;   

  

            default :   

  

              CPUType = "(unknown)";   

  

         }   

  

         listBox1.InsertItem (0,"Active Processor Mask :"+pSI.dwActiveProcessorMask.ToString());   

  

         listBox1.InsertItem (1,"Allocation Granularity :"+pSI.dwAllocationGranularity.ToString());   

  

         listBox1.InsertItem (2,"Number Of Processors :"+pSI.dwNumberOfProcessors.ToString());   

  

         listBox1.InsertItem (3,"OEM ID :"+pSI.dwOemId.ToString());   

  

         listBox1.InsertItem (4,"Page Size:"+pSI.dwPageSize.ToString());   

  

         listBox1.InsertItem (5,"Processor Level Value:"+pSI.dwProcessorLevel.ToString());   

  

         listBox1.InsertItem (6,"Processor Revision:"+ pSI.dwProcessorRevision.ToString());   

  

         listBox1.InsertItem (7,"CPU type:"+CPUType);   

  

         listBox1.InsertItem (8,"Maximum Application Address: "+pSI.lpMaximumApplicationAddress.ToString());   

  

         listBox1.InsertItem (9,"Minimum Application Address:" +pSI.lpMinimumApplicationAddress.ToString());   

  

         /************** 從 GlobalMemoryStatus 獲取返回值****************/   

  

         MEMORYSTATUS memSt = new MEMORYSTATUS ();   

         GlobalMemoryStatus (ref memSt);  

  

         listBox1.InsertItem(10,"Available Page File :"+ (memSt.dwAvailPageFile/1024).ToString ());   

  

         listBox1.InsertItem(11,"Available Physical Memory : " + (memSt.dwAvailPhys/1024).ToString());   

  

         listBox1.InsertItem(12,"Available Virtual Memory:" + (memSt.dwAvailVirtual/1024).ToString ());   

  

         listBox1.InsertItem(13,"Size of structur :" + memSt.dwLength.ToString());   

  

         listBox1.InsertItem(14,"Memory In Use :"+ memSt.dwMemoryLoad.ToString());   

  

         listBox1.InsertItem(15,"Total Page Size :"+ (memSt.dwTotalPageFile/1024).ToString ());   

  

         listBox1.InsertItem(16,"Total Physical Memory :" + (memSt.dwTotalPhys/1024).ToString());   

  

         listBox1.InsertItem(17,"Total Virtual Memory :" + (memSt.dwTotalVirtual/1024).ToString ());   

  

       }   

  

       catch(Exception er)   

  

       {   

  

         MessageBox.Show (er.Message);   

  

       }   

  

    }   

  

    public static void Main(string[] args)   

  

    {   

  

      try   

  

       {   

  

          Application.Run(new Form1());   

  

       }   

  

       catch(Exception er)   

  

       {   

  

          MessageBox.Show (er.Message );   

  

       }   

  

   }   

  

  }   

  

}   

 

 

 

 

C#中調用Windows   API的要點     

  日期:2003年12月11日   作者:佚名   人氣:586   查看:[大字體   中字體   小字體]       

      

  在.Net   Framework   SDK文檔中,關於調用Windows   API的指示比較零散,並且其中稍全面一點的是針對Visual   Basic   .net講述的。本文將C#中調用API的要點彙集如下,希望給未在C#中使用過API的朋友一點幫助。另外如果安裝了Visual   Studio   .net的話,在C:/Program   Files/Microsoft   Visual   Studio   .NET/FrameworkSDK/Samples/Technologies/Interop/PlatformInvoke/WinAPIs/CS目錄下有大量的調用API的例子。   

  一、調用格式   

  using   System.Runtime.InteropServices;   //引用此名稱空間,簡化後面的代碼   

  ...   

  //使用DllImportAttribute特性來引入api函數,注意聲明的是空方法,即方法體爲空。   

  [DllImport("user32.dll")]   

  public   static   extern   ReturnType   FunctionName(type   arg1,type   arg2,...);   

  //調用時與調用其他方法並無區別   

    

  可以使用字段進一步說明特性,用逗號隔開,如:   

  [   DllImport(   "kernel32",   EntryPoint="GetVersionEx"   )]     

  DllImportAttribute特性的公共字段如下:   

  1、CallingConvention   指示向非託管實現傳遞方法參數時所用的   CallingConvention   值。     

      CallingConvention.Cdecl   :   調用方清理堆棧。它使您能夠調用具有   varargs   的函數。   

      CallingConvention.StdCall   :   被調用方清理堆棧。它是從託管代碼調用非託管函數的默認約定。   

  2、CharSet   控制調用函數的名稱版本及指示如何向方法封送   String   參數。   

      此字段被設置爲   CharSet   值之一。如果   CharSet   字段設置爲   Unicode,則所有字符串參數在傳遞到非託管實現之前都轉換成   Unicode   字符。這還導致向   DLL   EntryPoint   的名稱中追加字母“W”。如果此字段設置爲   Ansi,則字符串將轉換成   ANSI   字符串,同時向   DLL   EntryPoint   的名稱中追加字母“A”。大多數   Win32   API   使用這種追加“W”或“A”的約定。如果   CharSet   設置爲   Auto,則這種轉換就是與平臺有關的(在   Windows   NT   上爲   Unicode,在   Windows   98   上爲   Ansi)。CharSet   的默認值爲   Ansi。CharSet   字段也用於確定將從指定的   DLL   導入哪個版本的函數。CharSet.Ansi   和   CharSet.Unicode   的名稱匹配規則大不相同。對於   Ansi   來說,如果將   EntryPoint   設置爲“MyMethod”且它存在的話,則返回“MyMethod”。如果   DLL   中沒有“MyMethod”,但存在“MyMethodA”,則返回“MyMethodA”。對於   Unicode   來說則正好相反。如果將   EntryPoint   設置爲“MyMethod”且它存在的話,則返回“MyMethodW”。如果   DLL   中不存在“MyMethodW”,但存在“MyMethod”,則返回“MyMethod”。如果使用的是   Auto,則匹配規則與平臺有關(在   Windows   NT   上爲   Unicode,在   Windows   98   上爲   Ansi)。如果   ExactSpelling   設置爲   true,則只有當   DLL   中存在“MyMethod”時才返回“MyMethod”。   

    

  3、EntryPoint   指示要調用的   DLL   入口點的名稱或序號。     

      如果你的方法名不想與api函數同名的話,一定要指定此參數,例如:   

  [DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")]   

  public   static   extern   int   MsgBox(IntPtr   hWnd,string   txt,string   caption,   int   type);   

    

  4、ExactSpelling   指示是否應修改非託管   DLL   中的入口點的名稱,以與   CharSet   字段中指定的   CharSet   值相對應。如果爲   true,則當   DllImportAttribute.CharSet   字段設置爲   CharSet   的   Ansi   值時,向方法名稱中追加字母   A,當   DllImportAttribute.CharSet   字段設置爲   CharSet   的   Unicode   值時,向方法的名稱中追加字母   W。此字段的默認值是   false。     

  5、PreserveSig   指示託管方法簽名不應轉換成返回   HRESULT、並且可能有一個對應於返回值的附加   [out,   retval]   參數的非託管簽名。     

  6、SetLastError   指示被調用方在從屬性化方法返回之前將調用   Win32   API   SetLastError。   true   指示調用方將調用   SetLastError,默認爲   false。運行時封送拆收器將調用   GetLastError   並緩存返回的值,以防其被其他   API   調用重寫。用戶可通過調用   GetLastWin32Error   來檢索錯誤代碼。   

    

  二、參數類型:   

  1、數值型直接用對應的就可。(DWORD   ->   int   ,   WORD   ->   Int16)   

  2、API中字符串指針類型   ->   .net中string   

  3、API中句柄   (dWord)     ->   .net中IntPtr   

  4、API中結構       ->   .net中結構或者類。注意這種情況下,要先用StructLayout特性限定聲明結構或類   

  公共語言運行庫利用StructLayoutAttribute控制類或結構的數據字段在託管內存中的物理佈局,即類或結構需要按某種方式排列。如果要將類傳遞給需要指定佈局的非託管代碼,則顯式控制類佈局是重要的。它的構造函數中用LayoutKind值初始化   StructLayoutAttribute   類的新實例。   LayoutKind.Sequential   用於強制將成員按其出現的順序進行順序佈局。   

  LayoutKind.Explicit   用於控制每個數據成員的精確位置。利用   Explicit,   每個成員必須使用   FieldOffsetAttribute   指示此字段在類型中的位置。如:   

  [StructLayout(LayoutKind.Explicit,   Size=16,   CharSet=CharSet.Ansi)]   

  public   class   MySystemTime     

  {   

          [FieldOffset(0)]public   ushort   wYear;     

          [FieldOffset(2)]public   ushort   wMonth;   

          [FieldOffset(4)]public   ushort   wDayOfWeek;     

          [FieldOffset(6)]public   ushort   wDay;     

          [FieldOffset(8)]public   ushort   wHour;     

          [FieldOffset(10)]public   ushort   wMinute;     

          [FieldOffset(12)]public   ushort   wSecond;     

          [FieldOffset(14)]public   ushort   wMilliseconds;     

  }   

  下面是針對API中OSVERSIONINFO結構,在.net中定義對應類或結構的例子:   

  /**********************************************   

  *   API中定義原結構聲明   

  *   OSVERSIONINFOA   STRUCT   

  *     dwOSVersionInfoSize       DWORD             ?   

  *     dwMajorVersion                 DWORD             ?   

  *     dwMinorVersion                 DWORD             ?   

  *     dwBuildNumber                   DWORD             ?   

  *     dwPlatformId                     DWORD             ?   

  *     szCSDVersion                     BYTE   128   dup   (?)   

  *   OSVERSIONINFOA   ENDS   

  *   

  *   OSVERSIONINFO     equ     <OSVERSIONINFOA>   

  *********************************************/   

    

  //.net中聲明爲類   

  [   StructLayout(   LayoutKind.Sequential   )]         

  public   class   OSVersionInfo     

  {         

          public   int   OSVersionInfoSize;   

          public   int   majorVersion;     

          public   int   minorVersion;   

          public   int   buildNumber;   

          public   int   platformId;   

    

          [   MarshalAs(   UnmanagedType.ByValTStr,   SizeConst=128   )]           

          public   String   versionString;   

  }   

  //或者   

  //.net中聲明爲結構   

  [   StructLayout(   LayoutKind.Sequential   )]       

  public   struct   OSVersionInfo2     

  {   

          public   int   OSVersionInfoSize;   

          public   int   majorVersion;     

          public   int   minorVersion;   

          public   int   buildNumber;   

          public   int   platformId;   

    

          [   MarshalAs(   UnmanagedType.ByValTStr,   SizeConst=128   )]           

          public   String   versionString;   

  }   

    

  此例中用到MashalAs特性,它用於描述字段、方法或參數的封送處理格式。用它作爲參數前綴並指定目標需要的數據類型。例如,以下代碼將兩個參數作爲數據類型長指針封送給   Windows   API   函數的字符串   (LPStr):     

          [MarshalAs(UnmanagedType.LPStr)]   

  String   existingfile;   

          [MarshalAs(UnmanagedType.LPStr)]   

  String   newfile;   

    

  注意結構作爲參數時候,一般前面要加上ref修飾符,否則會出現錯誤:對象的引用沒有指定對象的實例。   

  [   DllImport(   "kernel32",   EntryPoint="GetVersionEx"   )]     

  public   static   extern   bool   GetVersionEx2(   ref   OSVersionInfo2   osvi   );     

    

  三、如何保證使用託管對象的平臺調用成功?   

          如果在調用平臺   invoke   後的任何位置都未引用託管對象,則垃圾回收器可能將完成該託管對象。這將釋放資源並使句柄無效,從而導致平臺invoke   調用失敗。用   HandleRef   包裝句柄可保證在平臺   invoke   調用完成前,不對託管對象進行垃圾回收。   

          例如下面:   

                  FileStream   fs   =   new   FileStream(   "a.txt",   FileMode.Open   );   

                  StringBuilder   buffer   =   new   StringBuilder(   5   );   

                  int   read   =   0;   

                  ReadFile(fs.Handle,   buffer,   5,   out   read,   0   );   //調用Win   API中的ReadFile函數   

  由於fs是託管對象,所以有可能在平臺調用還未完成時候被垃圾回收站回收。將文件流的句柄用HandleRef包裝後,就能避免被垃圾站回收:   

  [   DllImport(   "Kernel32.dll"   )]   

  public   static   extern   bool   ReadFile(     

      HandleRef   hndRef,     

      StringBuilder   buffer,     

      int   numberOfBytesToRead,     

      out   int   numberOfBytesRead,     

      ref   Overlapped   flag   );   

  ......   

  ......   

                  FileStream   fs   =   new   FileStream(   "HandleRef.txt",   FileMode.Open   );   

                  HandleRef   hr   =   new   HandleRef(   fs,   fs.Handle   );   

                  StringBuilder   buffer   =   new   StringBuilder(   5   );   

                  int   read   =   0;   

                  //   platform   invoke   will   hold   reference   to   HandleRef   until   call   ends   

                  ReadFile(   hr,   buffer,   5,   out   read,   0   );  


發佈了11 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章