Chương 3: Để sử dụng Win API từ VBA

vbano1

SMod
Thành viên BQT
3.1 Chú ý khi sử dụng Win API
3.2 Cách khai báo hàm Win API
3.3 Kiểu dữ liệu của Win API và của VBA
3.4 Gọi hàm Win API
3.5 Chuỗi ký tự mà Win API sử dụng
3.6 Giá trị số mà Win API sử dụng
3.7 Kiểu dữ liệu do người dùng tự định nghĩa
3.8 Các điểm chính để sử dụng Win API
 

Euler

Mod
Thành viên BQT
3.1 Những điểm kỳ vọng khi sử dụng hàm Win API
Khi sử dụng hàm API, đây là các điểm mà chúng ta kỳ vọng.
Ⓐ Chúng ta muốn thêm vào chương trình VBA những tính năng mà các hàm cơ bản của VBA không có.
Ⓑ Chúng ta muốn tăng tốc độ xử lý của chương trình.
Ⓒ Muốn dùng ngôn ngữ cấp thấp.

Tuy nhiên Ⓑ và Ⓒ là các trường hợp rất đặc thù. Có lẽ chúng ta thường gặp trường hợp Ⓐ nhiều hơn.

Về trường hợp Ⓑ, có những trường hợp, việc gọi hàm API sẽ cho tốc độ nhanh hơn lệnh VBA. Tuy nhiên, hiệu quả có được như kỳ vọng hay không, thật ra là một điều khó đoán biết. Tại sao lại như vậy là vì, đối với VBA, chúng ta gọi các tính năng của Excel, tốc độ xử lý sẽ tương đồng với Excel.
Trong thực tế, những người tin tưởng tuyệt đối vào VBA sẽ cho rằng, VBA không chậm đâu, chẳng qua là code của bạn chậm thôi. Chẳng hạn nếu chúng ta không sử dụng phương thức Sort (sắp xếp) của Excel, thay vào đó ta sử dụng thuật toán nổi bọt để sắp xếp, hoặc chúng ta không sử dụng hàm MATCH của Excel để tìm kiếm, chúng ta sử dụng vòng lặp để tìm kiếm... tốc độ quả nhiên là có sự khác nhau. Cho nên có lý do để nói chương trình chậm là do cách code.

Tuy nhiên, VBA có những điểm hạn chế khi so với tốc độ thực thi của file EXE, quả nhiên tốc độ của VBA là chậm, trong quá trình xử lý, nếu nó phải tham chiếu tới thư viện biểu thức chính quy. Hay các thủ đoạn để giải quyết vấn đề của VBA có nhiều hạn chế, và có giới hạn. Lúc này, có lẽ chúng ta phải nghĩ tới việc thay thế phương án giải quyết.

Về điểm Ⓒ, đây là trường hợp rất đặc thù. Từ chương 4 trở đi, chúng ta nhìn vào các đoạn code mẫu, lúc đó các bạn sẽ hiểu nhưng tôi có thể nói rằng, hàm API là ngôn ngữ cấp thấp. Tuy nhiên, tôi không cho rằng đây là lý do chính để chúng ta sử dụng các hàm API.

Ta hãy xem ví dụ sau:
Mã:
Sub test1()
    MsgBox "Day la loi", vbCritical
End Sub
Kết quả:
Bạn cần đăng nhập để thấy đính kèm

Rõ ràng với code VBA như ở trên, các bạn thấy câu lệnh hết sức đơn giản. Tuy nhiên thực tế là lệnh VBA đã ngầm gọi hàm API để hiển thị DialogBox, kết quả các bạn nhìn thấy là hộp thoại cảnh báo hiện ra. Tuy nhiên sâu xa bên trong là các tính năng phức tạp mà code VBA đã giản tiện đi chỉ bằng một câu lệnh rất ngắn gọn. Vì vậy, có thể nói VBA là ngôn ngữ cao cấp (dễ hiểu), API là ngôn ngữ cấp thấp.

Ở đây, tôi muốn nói với các bạn rằng, API cũng giống như là rượu. Chúng ta không nên uống nhiều rươu, tức là không nên dùng nhiều hàm API. Đây là điểm mà chúng ta nên lưu ý. Đầu tiên, hãy tinh thông VBA. Hàm API nên là thủ đoạn cuối cùng nếu các bạn không còn cách nào khác. Thay vì dùng các hàm API, trong khả năng có thể, các bạn nên sử dụng càng nhiều hàm VBA cơ bản thì càng tốt.

Trong bản dịch trên, tôi đã lược dịch lấy các điểm chính-Lời người dịch.
 

giaiphapvba

Administrator
Thành viên BQT
3.2 Cách khai báo hàm API bằng lệnh Declare.
Ở các phần trước, chúng ta đã biết rằng hàm API được cung cấp trong các file DLL. Tuy nhiên, để gọi hàm API thì các điều dưới đây, VBA không hiểu.
Ⓐ Hàm API mà chúng ta muốn gọi nằm trong thư viện nào?
Ⓑ Ta phải truyền tham số như thế nào cho hàm API thì tốt?

Vì vậy, để truy cập vào DLL, ở VBA ta sử dụng lệnh Declare để khai báo hàm API.
Thông qua ví dụ về khai báo hàm lấy chỉ dẫn Windows Directory là GetWindowsDirectory, chúng ta sẽ cùng nhau học cách sử dụng lệnh Declare.
Đầu tiên hãy nhìn cấu trúc hàm GetWindowsDirectory được viết bằng ngôn ngữ C:
C++:
UNIT GetWindowsDirectoryA(
  LPSTR lpBuffer, //address of buffer for Windows directory
  UINT  uSize //size of directory buffer
);
Sau đây ta sẽ chuyển thành khai báo phù hợp để có thể sử dụng được trong VBA.
Đầu tiên hãy xem định dạng Win64 API.
Bạn cần đăng nhập để thấy đính kèm

❶ Khai báo của 64bit
Khi gọi Win64 API, đầu tiên chúng ta sử dụng từ khóa PtrSafe, nó sẽ biên dịch cho VBA 64bit.
❷ Chỉ định tên hàm
Tên hàm ở đây sẽ dùng giống như tên hàm được khai báo bằng ngôn ngữ C. Do đó chúng ta giữ nguyên tên hàm là GetWindowsDirectory.

Chú ý: Cũng có những hàm API không có giá trị trả về. Khi đó chúng ta không dùng Function, chúng ta dùng từ khóa Sub. (Lời người dịch: Hãy nhớ lại khái niệm thủ tục và hàm)

❸ Chỉ định thư viện
Hàm API này đang được cất trong DLL nào, chúng ta phải khai báo. Nếu không khai báo thì VBA sẽ không hiểu địa chỉ của hàm API. Như tôi đã nói, chúng ta giới hạn trong khuôn khổ 3 DLL phổ biến hay dùng nhất, đó là Kernel32.dll, User32.dll, Gdi32.dll , chúng ta có thể giản lược bỏ đuôi file là ".dll" và có thể viết là [Lib "kernel32"] nhưng khi bạn gọi hàm API trong thư viện khác thì phải viết rõ ràng cả đuôi file ".dll", ví dụ [Lib "advapi32.dll"] .
❹ Chỉ định tên hàm chính thức được công khai ở DLL
Ở mục 5 chương 3 (3.5) tôi sẽ giải thích về Win API sử dụng chuỗi ký tự nhưng cũng có trường hợp API được công khai bằng cái tên khác trong DLL. Trong trường hợp đó, ta sẽ khai báo tên hàm được công khai bằng Alias.
Tóm lại, hàm GetWindowsDirectory được công khai trong DLL là hai tên hàm: GetWindowsDirectoryAGetWindowsDirectoryW. Tên hàm GetWindowsDirectory không tồn tại trong thực tế.
Trong trường hợp ví dụ này, mặc dù có hai cái tên, nhưng chúng ta đã chọn khai báo lấy tên là GetWindowsDirectoryA.

Chú ý: Ở cuối tên hàm có sự khác biệt chữ cái [A] và [W], về điều này tôi sẽ thuyết minh trong mục Win API sử dụng chuỗi ký tự.

Ngoài ra, tên hàm API có phân biệt chữ in hoa và chữ thường. Trường hợp khai báo như dưới đây sẽ sinh ra lỗi.
Bạn cần đăng nhập để thấy đính kèm


Tổng kết: Thực tế là, nếu bạn chỉ định tên hàm API đúng ở phần Alias, thì đằng sau từ khóa Function, bạn có thể viết tên hàm tùy ý mà không phải là [GetWindowsDirectory]. Ví dụ, trường hợp khai báo như dưới đây cũng đúng.
Bạn cần đăng nhập để thấy đính kèm


Tuy nhiên, chúng ta không nên đặt tên tùy tiện như vậy, đó không phải là thói quen tốt. Hãy đặt tên giống như Microsoft.

❺ Chỉ định tham số
Trong C, tham số lpBuffer có kiểu dữ liệu được khai báo là [LPTSTR] nhưng trong VBA không có kiểu dữ liệu như vậy. Ở đây, ta chuyển thành khai báo kiểu String.
Tương tự như thế, kiểu dữ liệu [UNIT] chúng ta chuyển thành kiểu Long. Ở phần tiếp theo, tôi sẽ giới thiệu quy tắc chuyển đổi kiểu dữ liệu từ ngôn ngữ C sang kiểu dữ liệu của VBA.

❻ Giá trị trả về
Trong ngôn ngữ C, giá trị trả về của hàm GetWindowsDirectory là kiểu dữ liệu UNIT. Trong VBA, ta chuyển kiểu dữ liệu trả về của [Function GetWindowsDirectory] là kiểu LongPtr.
(Còn nữa)
 

Euler

Mod
Thành viên BQT
3.2 Cách khai báo hàm API bằng lệnh Declare.
(tiếp theo)
Ghi chú: Long và LongPtr
LongPtr không phải là kiểu dữ liệu cố định, trên 32bit thì nó là Long, trên 64bit thì nó là LongLong.
Chỉ cần nhớ điều này, chúng ta sẽ không khó khăn khi phải khai báo hàm API trên 32bit và 64bit.
  • Win64 API thì dùng LongPtr
  • Win32 API thì dùng Long
Ngoài ra trong trường hợp Win64 API, [UNIT] ở ❺ là kiểu Long, [UNIT] ở ❻ là LongPtr, nhìn qua thì chẳng ăn khớp gì với nhau, nhưng OS 32 bit từ Windows 95 trở đi, đối với hàm API, nếu tham số các bạn chỉ định là Long, thì kiểu dữ liệu trả về của hàm API dù ghi là LongPtr thì cũng không sao đâu.
Tại sao lại vậy thì ở chương 4 trở đi, khi các bạn nhìn vào các đoạn code mẫu các bạn sẽ hiểu. Đây là cách viết code vắn tắt khi chúng ta cần hoán đổi nhau giữa WIN32 API và WIN64 API.
Tất nhiên, tôi nghĩ cũng có ý kiến phản đối cách suy nghĩ này, vậy thì các bạn cứ viết thống nhất kiểu dữ liệu của tham số truyền vào và kiểu dữ liệu trả về của hàm API cũng được, hãy cứ làm như vậy đi.
Vậy, bây giờ chúng ta hãy nhìn vào cách khai báo hàm GetWindowsDirectory cho Win32 API.
Bạn cần đăng nhập để thấy đính kèm

❶ Không chỉ định từ khóa PtrSafe
Đối với trường hợp Win32 API, từ khóa PtrSafe là không cần thiết, cho nên không cần ghi từ khóa này khi khai báo.
❷ Chỉ định kiểu dữ liệu cho giá trị trả về của hàm
Win32 API không thể sử dụng LongPtr, do đó giá trị trả về của [Function GetWindowsDirectory] ta chuyển thành kiểu Long trên VBA. Có thể nói rằng giá trị trả về của Win32 API là 32bit (4 byte) do đó để kiểu Long là đương nhiên.

Ghi chú: Một số lưu ý khi sử dụng Declare
  1. Với lệnh Declare, ta cần viết khai báo ở trên đầu Module.
  2. Khi khai báo bằng Declare thì hàm (thủ tục) trong DLL sẽ được mặc định là Public. Tức là ở các Module khác trong VBA có thể gọi hàm API đã được khai báo này. Để các Module khác không thể gọi được nó, hãy dùng từ khóa Private.

Ngoài ra, nếu hàm API được khai báo không phải trên Module tiêu chuẩn (chẳng hạn trên UserForm) thì dù có sử dụng từ khóa Public, thì nó vẫn được nhìn nhận là Private.
 

tuhocvba

Administrator
Thành viên BQT
3.3 Kiểu dữ liệu của Win API và kiểu dữ liệu của VBA
Đương nhiên kiểu dữ liệu của ngôn ngữ C và VBA sẽ có sự khác nhau. Lý tưởng là chúng ta học và nắm được ngôn ngữ C, tuy nhiên tôi sẽ hệ thống những kiểu dữ liệu hay gặp nhất, và xây dựng thành bảng tương đối giữa kiểu dữ liệu Win API và VBA tương ứng.
Trong đó, kiểu dữ liệu này các bạn không cần quan tâm tới Win 64 API, chỉ cần cho Win 32 API là đủ rồi.
Có chú ý là, với kiểu dữ liệu BOOL của ngôn ngữ C, khi chuyển sang VBA sẽ là Long, các bạn lưu ý nhé.
Ngôn ngữ CNgôn ngữ VBA
ATOMInteger
BOOLLong
BYTEByte
CHARByte
COLORREFLong
DWORDLong
HWND, HDC, HMENU, Windows handle...Long
INT, UNITLong
LPARAMLong
LPDWORDLong
LPINT, LPUNITLong
LPRECTKiểu dữ liệu người dùng tự định nghĩa
LPSTR, LPCSTRString
LPVOIDAny
LPWORDInteger
LRESULTLong
NULLAny hoặc long
SHORTInteger
WORDInteger
WPARAMLong
LongLong
LPVOIDAny
 

Yukino Ichikawa

Thành viên
3.4 Gọi hàm (thủ tục) API
Hàm (và thủ tục) API được khai báo bằng câu lệnh Declare, thông thường có thể được gọi từ chương trình VBA để chúng thực thi.
Ở phần trước chúng ta đã được xem một ví dụ thực tế về khai báo hàm GetWindowsDirectory. Bây giờ chúng ta sẽ cùng xem xét nó được gọi ra như thế nào.

Đầu tiên, hãy xem trường hợp Win64 API.
Mã:
Declare PtrSafe Function GetWindowsDirectory Lib "kernel32" _
        Alias "GetWindowsDirectoryA" _
        (ByVal lpBuffer As String, _
        ByVal nSize As Long) As LongPtr
        
Public Const MAX_PATH = 260 'Do dai lon nhat cua PATH

Sub GetWindowsDirectory_Sample()
    'Windows Derectory Path Name
    Dim strBuffer   As String * MAX_PATH
    Dim rc          As LongPtr
    
    Dim strPrompt   As String
    
    'Lay WindowsDirectory Path Name
    rc = GetWindowsDirectory(strBuffer, Len(strBuffer))
    
    'Trich xuat lay phan ky tu can thiet tu data lay duoc
    strPrompt = Left(strBuffer, InStr(strBuffer, vbNullChar) - 1)
    
    MsgBox "Windows directory Path Name: " & strPrompt
End Sub
Hãy để ý vào các chỗ khai báo kiểu LongPtr trong toàn bộ chương trình trên.

Kết quả khi chạy thủ tục GetWindowsDirectory_Sample ta sẽ được thông báo có nội dung sau:
Mã:
Windows directory Path Name: C:\Windows
Đối với Win32 API, ta sửa từ khóa LongPtr thành Long. Từ khóa PtrSafe không cần thiết có thể bỏ đi (giữ lại cũng không sao).
Mã:
Declare Function GetWindowsDirectory Lib "kernel32" _
        Alias "GetWindowsDirectoryA" _
        (ByVal lpBuffer As String, _
        ByVal nSize As Long) As Long
        
Public Const MAX_PATH = 260 'Do dai lon nhat cua PATH

Sub GetWindowsDirectory_Sample()
    'Windows Derectory Path Name
    Dim strBuffer   As String * MAX_PATH
    Dim rc          As Long
    
    Dim strPrompt   As String
    
    'Lay WindowsDirectory Path Name
    rc = GetWindowsDirectory(strBuffer, Len(strBuffer))
    
    'Trich xuat lay phan ky tu can thiet tu data lay duoc
    strPrompt = Left(strBuffer, InStr(strBuffer, vbNullChar) - 1)
    
    Msgbox "Windows directory Path Name: " & strPrompt
End Sub
Chú ý, từ nay khi chúng tôi viết code, chúng tôi sẽ sử dụng tên biến là rc có ý nghĩa nó là từ giản lược của từ return code, dùng để lưu giá trị trả về của hàm API.

Ghi chú: VBA không kiểm tra code của hàm API có thể hoạt động hay không khi gọi hàm API.
Trong môi trường VBA, khi làm chương trình, có chức năng kiểm tra code đúng hay sai. Dù chỉ là truyền kiểu dữ liệu sai cho tham số thì quá trình thực thi macro sẽ thất bại và kết thúc ngay.
Tuy nhiên trong trường hợp VBA gọi hàm API, môi trường VBA không hỗ trợ xác nhận code hàm API đúng sai hay không.
Nói cách khác, nếu bạn chỉ định kiểu dữ liệu sai cho tham số của hàm API, khi thực thi có trường hợp hệ thống sẽ xảy ra lỗi, hoặc hàm API hoàn hoàn toàn không hoạt động.
 

giaiphapvba

Administrator
Thành viên BQT
3.5 Hàm API sử dụng chuỗi ký tự
VBA chuyển UNICODE thành ANSI và truyền cho API

Đối với hàm Win API chứa chuỗi ký tự thì có hai hình thức là ANSI và DBCS với Unicode.
Thực tế ở bài học trước, khi nói tới hàm GetWindowsDirectory chúng ta có tên hai hàm số sau được công khai là GetWindowsDirectoryAGetWindowsDirectoryW.
Chú ý: ANSI là kiểu ký tự ASCII, độ dài là 1 byte. Unicode là kiểu ký tự bao gồm các ký tự ASCII và các ký tự khác, có độ dài là 2 byte.

Như vậy, ở đây tồn tại một vấn đề. Ngôn ngữ VBA và hàm Win API của Windows có sự khác nhau về thiết định ký tự được sử dụng.
Môi trườngThiết định ký tự được sử dụng
VBAUnicode
Windows 95/98/ Me APIANSI
Windows NT 4.0/2000/XP/VISTA/7/8/10 APIANSI & Unicode

Nhìn vào biểu đồ trên, ta thấy VBA là Unicode. Tuy nhiên ký tự được thiết định của hàm API Windows 95/98/Me là ANSI. Câu chuyện ở đây có một chút phức tạp, nhưng chúng ta cũng không cần hiểu sâu hơn. Chỉ cần hiểu là, khi VBA gọi API thì tham số nếu là chuỗi ký tự thì chúng chuyển đổi thành ANSI rồi mới truyền cho API.

Ngoài ra, Windows NT 4.9/2000/XP/VISTA/7/8/10 API là Unicode nhưng hàm API khi tiếp nhận tham số từ VBA, đầu tiên VBA không quan tâm tới Unicode mà sẽ chuyển ký tự tham số đó thành ANSI.
Bạn cần đăng nhập để thấy đính kèm

(Còn nữa)
 

Yukino Ichikawa

Thành viên
3.5 Hàm API sử dụng chuỗi ký tự
(tiếp theo)
Tóm lại, chúng ta không cần phải quan tâm tới chuỗi ký tự truyền vào API của OS như thế nào. Nói đúng hơn là VBA sau khi tự ý chuyển đổi thành ANSI. Thông thường sẽ phải sử dụng tên hàm có chứa chữ A ở cuối tên hàm.
Lại nói về giá trị trả vè của hàm API là chuỗi ký tự, khi giá trị này được trả về trong môi trường VBA, nó sẽ nhanh chóng được chuyển đổi lại thành Unicode. Đây là điểm cần chú ý.
Tóm lại, khi xử lý chuỗi ký tự thu được từ API các hàm xử lý chuỗi ký tự bằng đơn vị Byte như LenB, LeftB, RightB, MidB không thể dùng được. Thông thường sẽ sử dụng các hàm Len, Left, Right, Mid.
Các hàm được công khai trong ANSI version, Unicode version chỉ là các hàm xử lý chuỗi ký tự. Do vậy, khi khai báo các hàm API sử dụng chuỗi ký tự, nhất định chúng ta phải dùng Alias để chỉ định tên hàm chính xác có chứa chữ [A] ở cuối tên hàm.

Hàm API sẽ ghi đè giá trị lên tham số chuỗi ký tự được truyền cho nó

Trong hàm API, chuỗi ký tự với tư cách là tham số được truyền vào có thể bị thay đổi. Trong trường hợp sử dụng hàm API như vậy, trong chương trình VBA phải đảm bảo độ dài cần thiết để truyền tham số chuỗi ký tự cho hàm API.
Mã:
Public Const MAX_PATH =260
Dim strBuffer As String * MAX_PATH 'Qui dinh do dai chuoi ky tu la 260
Chuỗi ký tự mà hàm API thay đổi có thể có kích thước dài hơn chuỗi ký tự tham số ban đầu được nhận vào, nếu vượt quá độ dài của biến số chuỗi ký tự, việc ghi đè dữ liệu có thể gây nên những lỗi, phá hủy hay làm hỏng các dữ liệu khác.
Bạn cần đăng nhập để thấy hình ảnh

(Còn nữa)
 

vbano1

SMod
Thành viên BQT
3.5 Hàm API sử dụng chuỗi ký tự
(tiếp theo)
Ở bài trên ta cần giải thích kỹ hơn về số 260. Tại sao ta lại phải quy định chuỗi ký tự chỉ có độ dài tối da tới 260 ký tự. Điều này là vì nó đã được định nghĩa trong ngôn ngữ C, cụ thể là ở file winder.h, chuỗi ký tự mà ta thường dùng trong VBA có độ dài rất lớn và không thể truyền cho hàm API được. Vì hàm API được viết bằng ngôn ngữ C. Do đó khi truyền chuỗi ký tự từ VBA sang ngôn ngữ C ta cần lấp đầy bằng data Null (mã ký tự là 0) để truyền cho API.
Trong trường hợp này, ta đã cố định độ dài chuỗi ký tự từ trước (260), do đó nó sẽ tự động được lấp đầy bởi Null.
Từ nay về sau, trong các ví dụ tôi đưa ra thì sẽ tuân thủ quy tắc này, sẽ cố định độ dài chuỗi ký tự ngay từ đầu.
Tuy nhiên, nếu một lúc nào đó, bạn dùng chuỗi ký tự thông thường, thì hãy nhớ là phải lấp đầy nó bằng Null như ví dụ dưới đây.
Mã:
Sub macro1()
    Dim strBuffer   As String 'Bien so co do dai lon
    '
    '
    '
    strBuffer = String(260, vbNullChar)
    '
    '
    '
End Sub
Hoặc:
Mã:
strBuffer = String(260, 0)
Bạn cần đăng nhập để thấy đính kèm

(Còn nữa)
 

Euler

Mod
Thành viên BQT
3.5 Hàm API sử dụng chuỗi ký tự
(tiếp theo)
Lấy chuỗi ký tự mà API đã tiếp nhận
Hàm API tiếp nhận chuỗi ký tự đã được lấp đầy bởi các ký tự Null, và thay đổi chuỗi ký tự đó, trả về cho hàm VBA.
Đối với hàm GetWindowsDirectory đã nói ở các bài viết trước, nó sẽ ghi dữ liệu vào chuỗi ký tự và trả về cho VBA là [C\Windows], thế nhưng, nếu giữ nguyên giá trị đó để sử dụng trong VBA thì không thể được.
Nhìn vào thì thấy là [C\Windows] nhưng thực tế ở phía sau đó là một đống ký tự Null.
Vì vậy, đối với tham số mà hàm API dùng để tiếp nhận chuỗi ký tự, nếu chúng ta lấy về chuỗi ký tự đó dùng trong VBA thì cần phải xử lý như dưới đây:
Mã:
strPrompt = Left(strBuffer, Instr(strBuffer, vbNullChar)-1)
Trong chuỗi ký tự strBuffer, ta cần tìm vị trí của ký tự Null đầu tiên ở bên trái. Từ vị trí này lùi 1 vị trí về bên trái là phần ý tự mà ta cần lấy. Vì vậy ta có thể dùng hàm Left để thực hiện công việc này.
Bạn cần đăng nhập để thấy đính kèm

Tuy nhiên cũng có người viết code như sau:
Mã:
rc = GetWindowsDirectory(strBuffer, Len(strBuffer))
strPrompt = Left(strBuffer, rc)
Thực tế câu lệnh này sẽ hoạt động bình thường trong VBA. Tuy nhiên nói một cách nghiêm khắc thì cách viết như thế này là sai.
Giá trị trả về rc là kích thước số lượng Byte của kiểu dữ liệu ANSI của strBuffer (đã được loại bỏ ký tự Null).
Giả sử như giá trị trả về của hàm GetWindowsDirectory là [C:\ウィンドウ] mà các ký tự đều là kiểu zenkaku. Trong trường hợp này thì với định dạng ANSI, chuỗi ký tự này sẽ được biểu diễn bằng 13 bytes. Như vậy rc sẽ là 13.
Câu lệnh trên trở thành:
Mã:
strPrompt = Left(strBuffer, 13)
Tuy nhiên với định dạng Unicode của VBA, thì hankaku/zenkaku sẽ được biểu diễn 1 ký tự là 2 bytes.
Với chuỗi ký tự strBuffer có 8 ký tự : [C:\ウィンドウ]. Như vậy là phải xử lý với tư cách là 16 bytes (mỗi ký tự là 2 byte).
Tức là để xử lý chính xác với chuỗi ký tự [C:\ウィンドウ] thì câu lệnh chính xác sẽ là:
Mã:
strPrompt = Left(strBuffer, 8)
Như thuyết minh ở trên, ta đã hiểu tại sao không thể dùng rc để thực thi xử lý đúng. Tại sao chúng ta nên xử lý bằng hàm Instr để xác định ký tự Null đầu tiên.
(Còn nữa)
 

giaiphapvba

Administrator
Thành viên BQT
3.5 Hàm API sử dụng chuỗi ký tự
(tiếp theo)
Khi truyền tham số chuỗi ký tự cho hàm API, chúng ta dùng Byval
Chương này thật là nhiều rắc rối đau đầu, nhưng không sao đâu. Khi đã sử dụng quen các hàm API, bạn sẽ thấy những điều này là bình thường.
Nào, hãy xem lại khai báo hàm API trong VBA.
Bạn cần đăng nhập để thấy đính kèm

Các khai báo chuỗi ký tự ở trên đều bắt đầu bằng ByVal.

Đối với môi trường VBA thì khi sử dụng để truyền tham số thì biến số vốn có ấy sẽ không bị ảnh hưởng.
Ví dụ: Hãy xem đoạn code sau:
Byval-VBA:
Sub main()
    Dim i As Integer
    i = 1
    S_change i
    MsgBox i 'Hien thi la 1
End Sub
Sub S_change(ByVal myno)
    myno = 2
End Sub
Mặc dù S_change có phép gán myno = 2 nhưng biến số myno chỉ là bản copy của biến số i trong thủ tục main.
Vì vậy ở thủ tục main, giá trị i không bị thay đổi, nó vẫn là 1.

Hãy thay đổi ví dụ trên:
Byref-VBA:
Sub main()
    Dim i As Integer
    i = 1
    S_change i
    MsgBox i 'Hien thi la 2
End Sub
Sub S_change(myno)
    myno = 2
End Sub
Khai báo thủ tục S_change ở trên tương đương với:
Mã:
Sub S_change(ByRef myno)
Trên đây là câu chuyện của môi trường VBA.
Tuy nhiên môi trường ngôn ngữ C thì không sử dụng như vậy.
Khi truyền tham số, biến số được trao cho hàm API sẽ là bản copy của biến số đó.
Biến số có hai thành phần quan trọng. Một là giá trị của biến số đó. Hai là địa chỉ bộ nhớ lưu trữ biến số đó.
Bạn cần đăng nhập để thấy đính kèm

Trong ngôn ngữ C, giá trị của biến số sẽ được gọi thông qua con trỏ chỉ tới địa chỉ của biến số. Trong VBA thì chúng ta không có khái niệm con trỏ.
Như ở hình minh họa trên, nếu ta không trao cho ngôn ngữ C địa chỉ con trỏ (1000) thì hàm API không thể thay đổi được giá trị chuỗi ký tự mà nó tiếp nhận.
Vậy làm thế nào để trao con trỏ cho hàm API? Ở phía VBA, ta cần khai báo bằng từ khóa ByVal. Khi đó ngôn ngữ C (API) sẽ tiếp nhận được con trỏ của biến số đó.
(Còn nữa)
 

vbano1

SMod
Thành viên BQT
3.5 Hàm API sử dụng chuỗi ký tự
(tiếp theo)
Truyền tham số string cho hàm DLL
Việc truyền tham số string cho hàm API là chủ đề đau đầu cho bất cứ ai.
Dưới đây là nội dung của Visual Basic 5.0 lấy từ .NET:
Thông thường khi truyền tham số string cho hàm API chúng ta sẽ sử dụng ByVal.
Ở Visual Basic sẽ sử dụng kiểu dữ liệu được gọi là BSTR, nó có tư cách như là kiểu dữ liệu String.
BSTR được cấu thành từ chính thể chuỗi ký tự với Header chứa thông tin chuỗi ký tự. Chuỗi ký tự cũng có thể được lấp đầy bởi các ký tự Null.
BSTR được truyền đi với tư cách như là con trỏ, hàm DLL có thể thay đổi chuỗi ký tự.
Con trỏ là biến số lưu trữ vị trí vùng nhớ của biến số khác, nó không phải là kiểu dữ liệu có trong thực tế.
BSTR là Unicode cho nên mỗi ký tự cần 2bytes để biểu diễn. Thông thường ký tự cuối cùng của BSTR là ký tự Null được biểu diễn bởi 2 bytes.
Bạn cần đăng nhập để thấy đính kèm


Hàm trong nhiều file DLL và tất cả các hàm của Windows API đều nhận thức LPSTR.

LPSTR là con trỏ, trỏ tới ký tự Null cuối cùng trong ngôn ngữ C (là kiểu ký tự ASCIIZ).
Ở đây chuỗi ký tự không có Header.
Ở hình minh họa dưới đây minh họa LPSTR trỏ tới ký tự ASCIIZ.
Bạn cần đăng nhập để thấy đính kèm


Trường hợp hàm DLL yêu cầu tham số là LPSTR (con trỏ trỏ tới ký tự Null cuối cùng), ta sẽ trao giá trị BSTR. Con trỏ trỏ tới BSTR là con trỏ trỏ tới byte đầu tiên của ký tự Null là ký tự kết thúc của của data string, ta có thể sử dụng giống như với LPSTR.

Lời người dịch: Mình đọc xong mà đầu óc cũng trở nên ù ù, chưa hiểu gì lắm.
Tóm lại, cho tới đây, các bạn chỉ cần ghi nhớ, đối với các hàm API có tham số là string, thì chúng ta sẽ khai báo bằng
ByVal.
 

NhanSu

Thành Viên Nổi Bật

Mình xin bổ sung bài của bạn @vbano1 . Khi ta khai báo một biến x bằng lệnh Dim thì địa chỉ của biến x (xác định bằng hàm VarPtr(x)) không đổi trong suốt quá trình chương trình chạy, chỉ có giá trị tại địa chỉ đó thay đổi được.
1. Nếu x = 1 là kiểu số Long (4 byte) chẳng hạn thì giá trị của 4 byte tại địa chỉ của biến x chính là giá trị của x.
Mã:
Public Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
    ByRef Destination As Any, _
    ByRef Source As Any, _
    ByVal Length As Long)
Sub test()
    Dim X As Long, Y As Long
    Dim PtrX As LongPtr
    X = 1000
    PtrX = VarPtr(X)  'Address of X
    CopyMemory Y, ByVal PtrX, 4           '(1)
    'CopyMemory Y, X, 4                         '(2)
    Debug.Print PtrX, VarPtr(PtrX), Y
End Sub
Kết quả khi chạy: 49801168 49801160 1000 (kết quả này có thể khác nhau đối với từng lần chạy hay trên máy khác). Hàm API CopyMemory sẽ copy Length bytes từ vùng nhớ có địa chỉ Source đến vùng nhớ có địa chỉ Destination. Ở đây ta chú ý thêm mặc dù CopyMemory được khai báo Byref nhưng khi lại được gọi bằng ByVal vì PtrX đang chứa địa chỉ của biến X (49801168), nếu ta gọi bằng ByRef thì địa chỉ của PtrX (49801160) được truyền cho hàm và ra kết quả sai; ta cũng có thể thay lệnh (1) thành (2), khi đó tham số X được truyền bằng ByRef nên địa chỉ của X (chính là PtrX=49801168) được truyền cho CopyMemory.
2. Nếu x = "abcde" là kiểu String thì giá trị của vùng nhớ tại địa chỉ của x không phải là nội dung chuỗi mà chứa địa chỉ của chuỗi đó, VBA cũng có hàm StrPtr để lấy địa chỉ của chuỗi này. VBA lưu chuỗi dưới dạng BSTR, đó là cấu trúc gồm 4 byte chứa độ dài của chuỗi tính theo byte (không tính ký tự null), theo sau là chuỗi unicode, mỗi ký tự chiếm 2 byte, cuối cùng là 2 byte ký tự null. VBA thường không quan tâm đến ký tự null này và ta có thể thay đổi nội dung 4 byte đầu để thay đổi độ dài chuỗi. Ví dụ chuỗi x="abcde" thì VBA sẽ BSTR sẽ lưu dưới dạng 4 byte đầu tạo thành số Long có giá trị = 10, tiếp đến 10 byte chuỗi abcde dạng unicode 16, cuối cùng là 2 byte null. Hàm StrPtr(x) sẽ trả về địa chỉ của nội dung chuỗi unicode (không phải địa chỉ bắt đầu của BSTR)
Mã:
Option Explicit
#If VBA7 Then
    Public Const PTR_LENGTH As Long = 8
#Else
    Public Const PTR_LENGTH As Long = 4
#End If
Public Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
    ByRef Destination As Any, _
    ByRef Source As Any, _
    ByVal Length As Long)
Sub test2()
    Dim X As String, i As Long
    Dim PtrX As LongPtr, PtrS As LongPtr
    Dim arr(1 To 10) As Byte             
    X = "abcde"
    PtrX = VarPtr(X)  'Address of X
    'CopyMemory PtrS, ByVal PtrX, PTR_LENGTH     'Address of "abcde"
    'CopyMemory arr(1), ByVal PtrS, 10
    CopyMemory arr(1), ByVal StrPtr(X), 10
    For i = 1 To 10
        Debug.Print arr(i)
    Next
End Sub[code]
Chạy sub Test2 ta được nội dung của chuỗi.
3. Qua các ví dụ trên, ta cũng thấy khi truyền tham số kiểu String cho API thì ta cần truyền dưới dạng ByVal vì nếu dùng Byref thì địa chỉ của địa chỉ của chuỗi sẽ được chuyển cho hàm.
 

Euler

Mod
Thành viên BQT
3.6 Hàm API sử dụng số
Chúng ta cùng phán đoán cách truyền tham số xem hàm API có ghi đè giá trị lên số được truyền cho nó hay không


Tham số truyền cho hàm API thì không chỉ có ký tự, đương nhiên có cả trường hợp tham số truyền cho hàm API là số.
Trường hợp là chuỗi ký tự, như chúng ta đã biết, hàm API sẽ ghi đè giá trị cho chuỗi ký tự bằng việc ở môi trường VBA, chúng ta khai báo bằng từ khóa ByVal.
Trường hợp là số, giá trị số đó sẽ được giữ nguyên, nói cách khác, hàm API không thể ghi đè thay đổi giá trị nếu biến số là số, tham số được truyền sẽ được khai báo bằng từ khóa ByVal.
Ở ví dụ trước đây khi chúng ta sử dụng hàm GetWindowsDirectory, hàm này tiếp nhận số 260 và không thể thay đổi giá trị này. Và tham số khi truyền cho hàm API được khai báo là [Byval nSize As Long].

Tuy nhiên, ở các phần sau, các bạn sẽ thấy hàm GetExitCodeProcess có thể thay đổi giá trị số mà nó tiếp nhận và trả về cho VBA, tham số được truyền khi khai báo không sử dụng từ khóa ByVal.
(Còn nữa)
 
Top