C# 調(diào)用 Win32 API的實(shí)現(xiàn)示例
一、核心概念解析
1. 什么是 Win32 API?
Win32 API(Windows 32-bit Application Programming Interface)是微軟為 Windows 操作系統(tǒng)提供的底層編程接口,包含了操作系統(tǒng)的核心功能(如窗口管理、文件操作、進(jìn)程控制、內(nèi)存管理、系統(tǒng)信息獲取等),本質(zhì)上是一組用 C/C++ 編寫的原生函數(shù)。
2. C# 為什么能調(diào)用 Win32 API?
C# 運(yùn)行在 .NET 運(yùn)行時(shí)(CLR)中,屬于托管代碼;而 Win32 API 是非托管代碼(直接運(yùn)行在操作系統(tǒng)層面)。.NET 提供了 P/Invoke(Platform Invocation Services,平臺(tái)調(diào)用服務(wù)) 機(jī)制,這是 CLR 提供的核心功能,允許托管代碼調(diào)用非托管的函數(shù)(如 Win32 API)。
3. P/Invoke 核心要素
要在 C# 中調(diào)用 Win32 API,必須滿足以下條件:
- 函數(shù)簽名匹配:C# 中聲明的函數(shù)必須和 Win32 API 的原生簽名(返回值、參數(shù)類型、調(diào)用約定)一致;
- DLL 導(dǎo)入:指定 Win32 API 所在的系統(tǒng) DLL(如
kernel32.dll、user32.dll、advapi32.dll等); - 數(shù)據(jù)類型映射:C/C++ 的原生類型(如
DWORD、HANDLE、LPCSTR)需要映射到 C# 對應(yīng)的類型(如uint、IntPtr、string)。
二、Win32 API 調(diào)用的語法規(guī)則
1. 基礎(chǔ)聲明格式
在 C# 中,通過 DllImport 特性(位于 System.Runtime.InteropServices 命名空間)聲明 Win32 API 函數(shù),核心語法如下:
using System.Runtime.InteropServices; // 必須引入此命名空間
class Win32Api
{
// DllImport 特性指定 API 所在的 DLL
[DllImport("DLL名稱",
CharSet = CharSet.ANSI/Unicode, // 字符集(匹配 API 要求)
SetLastError = true/false, // 是否捕獲系統(tǒng)錯(cuò)誤碼
CallingConvention = CallingConvention.StdCall)] // 調(diào)用約定(Win32 幾乎都是 StdCall)
// 方法聲明:必須是 static extern,返回值+方法名+參數(shù)列表(類型要匹配)
public static extern 返回值類型 方法名(參數(shù)類型1 參數(shù)1, 參數(shù)類型2 參數(shù)2, ...);
}2. 關(guān)鍵參數(shù)說明
| 特性參數(shù) | 作用 |
| DllName | Win32 API 所在的系統(tǒng) DLL 名稱(如 kernel32.dll、user32.dll) |
| CharSet | 字符編碼:CharSet.Ansi(ANSI)、CharSet.Unicode(UTF-16)、CharSet.Auto(自動(dòng)) |
| SetLastError | 設(shè)為 true 時(shí),可通過 Marshal.GetLastWin32Error() 獲取系統(tǒng)錯(cuò)誤碼 |
| CallingConvention | 調(diào)用約定:Win32 API 默認(rèn)為 StdCall(C# 默認(rèn)為 Winapi,等價(jià)于 StdCall) |
3. 常見類型映射(Win32 → C#)
Win32 API 的原生類型和 C# 類型必須嚴(yán)格映射,否則會(huì)導(dǎo)致調(diào)用失敗甚至程序崩潰:
| C# 等效類型 | 說明 | ||
| DWORD | uint | 32 位無符號整數(shù) | ||
| HANDLE | IntPtr | 句柄(指針類型,用 IntPtr 兼容 32/64 位) | ||
| LPCSTR | string | ANSI 字符串(常量指針) | ||
| LPWSTR | string | Unicode 字符串(可變指針) | ||
| BOOL | bool/int | Win32 的 BOOL 是 int(0 / 非 0),C# 可用 bool 兼容 | ||
| int/long | 直接映射 | ||
| VOID | void | 無返回值 |
三、控制臺(tái)實(shí)戰(zhàn)案例(多個(gè)場景)
環(huán)境準(zhǔn)備
- 開發(fā)工具:Visual Studio(任意版本)或 VS Code + .NET SDK
- 創(chuàng)建項(xiàng)目:控制臺(tái)應(yīng)用(.NET Framework/.NET Core/.NET 5+ 均可,示例用 .NET 8)
- 核心命名空間:
System.Runtime.InteropServices(必須)
案例 1:獲取系統(tǒng)目錄(簡單無參數(shù) / 返回值)
需求:調(diào)用 kernel32.dll 中的 GetSystemDirectory 函數(shù),獲取 Windows 系統(tǒng)目錄(如 C:\Windows\System32)。
步驟 1:查看 Win32 API 原生簽名
// Win32 原生聲明(C/C++) UINT GetSystemDirectoryA( LPSTR lpBuffer, // 接收目錄的緩沖區(qū) UINT uSize // 緩沖區(qū)大小 );
- 返回值:實(shí)際復(fù)制到緩沖區(qū)的字符數(shù)(不含終止符);
- 字符集:
A后綴表示 ANSI,W后綴表示 Unicode(推薦用 Unicode)。
步驟 2:C# 聲明并調(diào)用
using System;
using System.Runtime.InteropServices; // 核心命名空間
namespace Win32ApiDemo
{
class Program
{
// 1. 聲明 Win32 API(使用 Unicode 版本 GetSystemDirectoryW)
[DllImport("kernel32.dll", // API 所在 DLL
CharSet = CharSet.Unicode, // 匹配 W 后綴的 Unicode 版本
SetLastError = true)] // 啟用錯(cuò)誤碼捕獲
// static extern 是固定寫法,返回值 uint 對應(yīng) Win32 的 UINT
private static extern uint GetSystemDirectoryW(
char[] lpBuffer, // 字符數(shù)組作為緩沖區(qū)(替代 C 的 char*)
uint uSize // 緩沖區(qū)大小
);
static void Main(string[] args)
{
try
{
// 2. 準(zhǔn)備緩沖區(qū)(系統(tǒng)目錄最長不超過 260 字符,預(yù)留冗余)
char[] buffer = new char[256];
// 3. 調(diào)用 Win32 API
uint result = GetSystemDirectoryW(buffer, (uint)buffer.Length);
// 4. 處理結(jié)果
if (result == 0)
{
// 調(diào)用失敗,獲取錯(cuò)誤碼
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine($"調(diào)用失敗,錯(cuò)誤碼:{errorCode}");
}
else
{
// 將字符數(shù)組轉(zhuǎn)為字符串(去掉空字符)
string systemDir = new string(buffer).TrimEnd('\0');
Console.WriteLine($"Windows 系統(tǒng)目錄:{systemDir}");
}
}
catch (Exception ex)
{
Console.WriteLine($"異常:{ex.Message}");
}
Console.ReadKey();
}
}
}運(yùn)行結(jié)果
Windows 系統(tǒng)目錄:C:\Windows\System32
案例 2:彈出系統(tǒng)消息框(調(diào)用 user32.dll)
需求:調(diào)用 user32.dll 中的 MessageBox 函數(shù),彈出 Windows 原生消息框(控制臺(tái)程序也能調(diào)用 GUI 相關(guān) API)。
using System;
using System.Runtime.InteropServices;
namespace Win32ApiDemo
{
class Program
{
// 1. 聲明 MessageBoxW(Unicode 版本)
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int MessageBoxW(
IntPtr hWnd, // 父窗口句柄(控制臺(tái)無窗口,傳 IntPtr.Zero)
string lpText, // 消息內(nèi)容
string lpCaption, // 標(biāo)題
uint uType // 消息框類型(按鈕+圖標(biāo))
);
// 定義消息框類型常量(對應(yīng) Win32 的宏)
private const uint MB_OK = 0x00000000; // 僅 OK 按鈕
private const uint MB_ICONINFORMATION = 0x00000040; // 信息圖標(biāo)
private const uint MB_OKCANCEL = 0x00000001; // OK + 取消按鈕
static void Main(string[] args)
{
try
{
// 2. 調(diào)用 MessageBoxW
int ret = MessageBoxW(
IntPtr.Zero, // 無父窗口
"這是 C# 調(diào)用 Win32 API 彈出的消息框!", // 消息內(nèi)容
"Win32 API 演示", // 標(biāo)題
MB_OK | MB_ICONINFORMATION // 組合類型:OK 按鈕 + 信息圖標(biāo)
);
// 3. 處理返回值(用戶點(diǎn)擊的按鈕)
switch (ret)
{
case 1:
Console.WriteLine("用戶點(diǎn)擊了【確定】按鈕");
break;
case 2:
Console.WriteLine("用戶點(diǎn)擊了【取消】按鈕");
break;
default:
Console.WriteLine($"返回值:{ret}(調(diào)用失敗)");
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"異常:{ex.Message}");
}
Console.ReadKey();
}
}
}關(guān)鍵說明
MessageBoxW的返回值:1 = 確定,2 = 取消,3 = 終止,4 = 重試,5 = 忽略等;- 消息框類型可以通過
|組合(如MB_OK | MB_ICONWARNING表示 OK 按鈕 + 警告圖標(biāo)); - 控制臺(tái)程序調(diào)用 GUI API 時(shí),
hWnd傳IntPtr.Zero即可。
案例 3:獲取進(jìn)程 ID(調(diào)用 GetCurrentProcessId)
需求:調(diào)用 kernel32.dll 的 GetCurrentProcessId 獲取當(dāng)前控制臺(tái)程序的進(jìn)程 ID。
using System;
using System.Runtime.InteropServices;
namespace Win32ApiDemo
{
class Program
{
// 聲明 GetCurrentProcessId(無參數(shù),返回 DWORD)
[DllImport("kernel32.dll", SetLastError = false)] // 此函數(shù)不會(huì)失敗,無需捕獲錯(cuò)誤碼
private static extern uint GetCurrentProcessId();
static void Main(string[] args)
{
// 調(diào)用 API
uint pid = GetCurrentProcessId();
Console.WriteLine($"當(dāng)前控制臺(tái)程序的進(jìn)程 ID:{pid}");
// 驗(yàn)證:可以在任務(wù)管理器中查看控制臺(tái)程序的 PID 是否一致
Console.WriteLine("按任意鍵退出...");
Console.ReadKey();
}
}
}運(yùn)行結(jié)果
當(dāng)前控制臺(tái)程序的進(jìn)程 ID:12345
按任意鍵退出...
案例 4:讀寫 INI 文件
需求:調(diào)用 kernel32.dll 的 WritePrivateProfileString 和 GetPrivateProfileString 讀寫 INI 配置文件(Win32 原生 INI 操作)。
using System;
using System.Runtime.InteropServices;
namespace Win32ApiDemo
{
class Program
{
// 1. 聲明寫 INI 的 API
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool WritePrivateProfileStringW(
string lpAppName, // 節(jié)名(INI 的 [Section])
string lpKeyName, // 鍵名
string lpString, // 鍵值
string lpFileName // INI 文件路徑
);
// 2. 聲明讀 INI 的 API
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetPrivateProfileStringW(
string lpAppName, // 節(jié)名
string lpKeyName, // 鍵名
string lpDefault, // 默認(rèn)值(讀取失敗時(shí)返回)
char[] lpReturnedString, // 接收值的緩沖區(qū)
uint nSize, // 緩沖區(qū)大小
string lpFileName // INI 文件路徑
);
static void Main(string[] args)
{
string iniPath = $"{Environment.CurrentDirectory}\\demo.ini";
try
{
// 步驟 1:寫入 INI 文件
bool writeSuccess = WritePrivateProfileStringW(
"UserInfo", // 節(jié)名
"UserName", // 鍵名
"張三", // 鍵值
iniPath // 文件路徑
);
if (writeSuccess)
{
Console.WriteLine($"成功寫入 INI 文件:{iniPath}");
}
else
{
int errorCode = Marshal.GetLastWin32Error();
Console.WriteLine($"寫入失敗,錯(cuò)誤碼:{errorCode}");
}
// 步驟 2:讀取 INI 文件
char[] buffer = new char[1024];
uint readLen = GetPrivateProfileStringW(
"UserInfo", // 節(jié)名
"UserName", // 鍵名
"默認(rèn)值", // 默認(rèn)值
buffer, // 緩沖區(qū)
(uint)buffer.Length, // 緩沖區(qū)大小
iniPath // 文件路徑
);
if (readLen > 0)
{
string value = new string(buffer).TrimEnd('\0');
Console.WriteLine($"讀取到的值:{value}");
}
else
{
Console.WriteLine("讀取失敗或鍵不存在");
}
}
catch (Exception ex)
{
Console.WriteLine($"異常:{ex.Message}");
}
Console.ReadKey();
}
}
}運(yùn)行結(jié)果
成功寫入 INI 文件:D:\Win32ApiDemo\bin\Debug\net8.0\demo.ini
讀取到的值:張三
生成的 INI 文件內(nèi)容
[UserInfo] UserName=張三
四、常見問題與避坑指南
1. 調(diào)用失敗的常見原因
- 類型不匹配:如 Win32 的
DWORD用了 C# 的int(雖然有時(shí)能運(yùn)行,但 64 位系統(tǒng)會(huì)出問題); - 字符集錯(cuò)誤:調(diào)用
A后綴的 API 卻用CharSet.Unicode,或反之; - 調(diào)用約定錯(cuò)誤:Win32 API 幾乎都是
StdCall,若設(shè)為Cdecl會(huì)導(dǎo)致棧溢出; - 緩沖區(qū)大小不足:如獲取系統(tǒng)目錄時(shí)緩沖區(qū)太小,返回值為 0;
- 權(quán)限問題:部分 Win32 API 需要管理員權(quán)限(如修改系統(tǒng)設(shè)置),需右鍵以管理員運(yùn)行程序。
2. 如何調(diào)試 Win32 API 調(diào)用?
- 啟用
SetLastError = true,調(diào)用后通過Marshal.GetLastWin32Error()獲取錯(cuò)誤碼,對照 Windows 錯(cuò)誤碼表 排查; - 檢查 API 名稱是否正確(如是否漏寫
W/A后綴); - 用
IntPtr替代所有句柄類型(避免 32/64 位兼容性問題); - 在 try-catch 中捕獲
EntryPointNotFoundException(API 名稱錯(cuò)誤)、AccessViolationException(內(nèi)存訪問錯(cuò)誤)等異常。
到此這篇關(guān)于C# 調(diào)用 Win32 API的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)C# 調(diào)用 Win32 API內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于C# Math 處理奇進(jìn)偶不進(jìn)的實(shí)現(xiàn)代碼
下面小編就為大家?guī)硪黄P(guān)于C# Math 處理奇進(jìn)偶不進(jìn)的實(shí)現(xiàn)代碼。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-05-05
Unity實(shí)現(xiàn)主角移動(dòng)與攝像機(jī)跟隨
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)主角移動(dòng)與攝像機(jī)跟隨,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03
C# Winform 讓整個(gè)窗口都可以拖動(dòng)
Windows 的 API 果然強(qiáng)大啊.以前要實(shí)現(xiàn)全窗口拖動(dòng), 要寫鼠標(biāo)按下和抬起事件, 很是麻煩, 偶爾還會(huì)出現(xiàn) BUG2011-05-05

