Khởi động ứng dụng khác bằng VBA

Euler

Administrator
Thành viên BQT
Về cơ bản macro Excel sẽ thao tác trên Excel, đây là chức năng chính. Tuy nhiên, cũng có trường hợp chúng ta muốn thao tác với các ứng dụng ngoài Excel chẳng hạn như NotePad. Bài viết này tôi sẽ giới thiệu với các bạn điều đó.

1. Sử dụng hàm Shell để khởi động ứng dụng khác (Application)
Cách đơn giản nhất để khởi động ứng dụng khác đó là sử dụng hàm Shell.

Mã:
Sub Sample1()
    Dim rc As Long
    rc = Shell("notepad.exe", vbNormalFocus)
    If rc = 0 Then MsgBox "Viec khoi dong that bai"
End Sub
Bạn cần đăng nhập để thấy đính kèm

Chương trình Notepad ở đây là notepad.exe , thường được lưu ở C:\Windows\System32 .
Đây là thiết định mặc định của Window cho nên chỉ cần nêu tên ứng dụng notepad.exe là hàm Shell có thể khởi động được nó.
Tham số thứ hai trong code trên là vbNormalFocus . Nó có nghĩa như sau:
Ứng dụng NotePad sẽ được hiển thị tại vị trí trên cửa sổ Window và kích thước vốn có từ trước tới nay.

Mặt khác, hàm Shell nếu khởi động thành công ứng dụng khác, trong OS (hệ điều hành) sẽ được trả về trị số Task ID. Trị số Task ID này mỗi lần sẽ xuất hiện các giá trị khác nhau nhưng nếu thất bại thì trị số này sẽ là 0.

Thao tác với NotePad bằng VBA thông qua hàm Shell là việc khó. Các bạn có thể sử dụng hàm SendKeys để ra mệnh lệnh từ VBA ghi nội dung lên NotePad. Ví dụ:
Mã:
Sub Sample2()
    Dim rc As Long
    ActiveCell.Copy
    rc = Shell("notepad.exe", vbNormalFocus)
    If rc <> 0 Then
        Application.SendKeys "%EP", True
    Else
        MsgBox "Khoi dong that bai"
    End If
End Sub
Kết quả:
Bạn cần đăng nhập để thấy đính kèm


Trước khi hàm Shell được gọi để khởi động NotePad thì macro thực hiện copy data trong ActiveCell.
Sau khi NotePad được khởi động, bằng hàm SendKey chúng ta gửi Key Code là "%EP ". Điều này tương đương với thao tác bằng tay đó là bạn vừa ấn phím ALT vừa ấn phím E, tiếp theo ấn phím P.
Tóm lại nó tương đương với việc bạn vào Menu Edit và chọn Paste.

Tuy nhiên tùy từng điều kiện mà code trên cũng có thể thất bại. Hàm Shell sẽ khởi động NotePad nhưng VBA không thể phán đoán được việc khởi động này đã kết thúc thành công hay không. Trước khi việc mở NotePad thành công, câu lệnh SendKeys được thực thi sẽ dẫn tới là Excel (không phải NotePad) sẽ nhận được Key Code là "%EP ".
Do tốc độ CPU, do bộ nhớ RAM còn nhiều hay ít, thời gian khởi động ứng dụng khác sẽ khác nhau. Vì vậy code trên cũng có trường hợp thất bại là vì thế.

2. Khởi động ứng dụng có liên quan tới File.
Khi chúng ta double click vào icon của một file (ví dụ file pdf), ứng dụng đó mở nó sẽ được mở ra (ví dụ foxit reader).
Đây là vì Windows quản lý mối liên quan giữa Application và phần mở rộng của File.

Chúng ta dựa vào đặc điểm này để sử dụng WSH(Windows Script Host) mở ứng dụng khác rất đơn giản.
Ví dụ dưới đây, chúng ta sẽ thấy điều này:
Mã:
Sub Sample3()
    With CreateObject("Wscript.Shell")
        .Run "C:\Data\Image.JPG", 5
    End With
End Sub
Phương thức Run sẽ mở file có đường dẫn mà bạn truyền vào. Ứng dụng liên quan tới phần mở rộng JPG sẽ được kích hoạt mở ra.
Tham số thứ nhất chúng ta phải chỉ định đường dẫn file cho phương thức RUN.
Tham số thứ ba nếu được chỉ định, nó sẽ có giá trị True/False trong việc phán đoán quá trình thực thi Program đã kết thúc hay chưa.
Ở code trên ta đã giản lược không dùng tham số thứ ba.

Ví dụ này, chúng ta sẽ mở 3 file pdf.
Mã:
Sub Sample4()
    With CreateObject("Wscript.Shell")
        .Run "C:\Data\Sample1.pdf", 5
        .Run "C:\Data\Sample2.pdf", 5
        .Run "C:\Data\Sample3.pdf", 5
    End With
End Sub
Trong ví dụ tiếp theo, ta sẽ chỉ định cho tham số thứ ba là True. Khi file pdf thứ nhất kết thúc, thì file pdf thứ hai được mở, quá trình mở kết thúc thì file thứ 3 sẽ được mở.
Mã:
Sub Sample5()
    With CreateObject("Wscript.Shell")
        .Run "C:\Data\Sample1.pdf", 5, True
        .Run "C:\Data\Sample2.pdf", 5, True
        .Run "C:\Data\Sample3.pdf", 5, True
    End With
End Sub
Ngoài ra, khi chúng ta chỉ định tham số thứ 3 của phương thức RUN là TRUE, chúng ta cũng có thể lấy giá trị trả về của Program.
Ở ví dụ 6, chúng ta sẽ thực thi một file .bat, sau đó tiến hành phán đoán quá trình thực thi đó đã hoàn thành hay chưa.
Mã:
Sub Sample6()
    Dim ret As Long
    
    With CreateObject("Wscript.Shell")
        ret = .Run("C:\Data\DailyTask.bat", 7, True)
    End With
    
    If ret <> 0 Then MsgBox "That bai": Exit Sub 'Thất bại
    
    'Xử lý khác
End Sub
Nguồn:
Ở bài dịch trên chúng tôi chưa thấy nhắc tới tham số thứ hai: các số 5,7 trong các ví dụ trên có ý nghĩa gì? Chúng tôi chưa có thời gian tìm hiểu. Nếu bạn đọc biết, xin hãy phản hồi bổ sung giúp chúng tôi trong topic này. Chân thành cảm ơn.
 

NhanSu

SMod
Thành viên BQT
Tham số thứ 2 là intWindowStyle, xác định kiểu cửa sổ xuất hiện, có các giá trị sau (theo ):
- 0: ẩn cửa sổ
- 1: Kích hoạt cửa sổ với kích thước bình thường, nên sử dụng khi hiện cửa sổ lần đầu (tức là khi chạy chuơng trình)
- 2: Kích hoạt và thu nhỏ cửa sổ
- 3: Kích hoạt và phóng to tối đa cửa sổ
- 4: Hiện cửa sổ với kích thước và vị trí thường sử dụng, không chiếm quyền điều khiển (focus)
- 5: Kích hoạt cửa sổ với kích thước và vị trí bình thường
- 6: Thu nhỏ cửa sổ và kích hoạt cửa sổ kế tiếp
- 7: Thu nhỏ cửa sổ và giữ nguyên focus
- 8: HIện cửa sổ ở trạng thái hiện tại, giữ nguyên focus
- 9: Kích hoạt cửa sổ với kích thước bình thường, nên sử dụng sau khi cửa sổ này bị thu nhỏ
- 10: Đặt trạng thái cửa sổ phụ thuộc vào trạng thái của chương trình gọi (tức là nếu đang có 1 chuơng trình chạy ở chế độ thu nhỏ, thì lệnh Run với tham số 10 cũng tạo ra một cửa sổ thu nhỏ)
 
T

thanhphong

Guest
@NhanSu
Bác cho em hỏi: Nếu như có nhiều ứng dụng đang mở, trong đó có cả Notepad đang mở.
Vậy làm thế nào để Active Notepad và Sendkey cho nó?
 

NhanSu

SMod
Thành viên BQT
@thanhphong mình sử dụng FindWindow để tìm hWnd, dùng SetForegroundWindow để set focus cho cửa sổ.
Mã:
Public Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function SetForegroundWindow Lib "user32" (ByVal hWnd As LongPtr) As Long

Sub SendKeysToNotepad()
Dim hWnd As LongPtr
hWnd = FindWindow("Notepad", "a.txt - Notepad")
If hWnd = 0 Then End
SetForegroundWindow hWnd
Application.SendKeys ("Test")
End Sub
 

NhanSu

SMod
Thành viên BQT
Khi file mở trong Notepad đã bị thay đổi mà chưa lưu thì caption của cửa sổ có thêm dấu * ở đầu chuỗi. Vì vậy để tìm cửa sổ Notepad mở file a.txt, ta có thể phải tìm "a.txt - Notepad" và "*a.txt - Notepad".
 
T

thanhphong

Guest
Khi file mở trong Notepad đã bị thay đổi mà chưa lưu thì caption của cửa sổ có thêm dấu * ở đầu chuỗi. Vì vậy để tìm cửa sổ Notepad mở file a.txt, ta có thể phải tìm "a.txt - Notepad" và "*a.txt - Notepad".
Có cách nào For each để mình duyệt qua từng cửa sổ đang mở không anh.
 

NhanSu

SMod
Thành viên BQT
Mình dựa theo duyệt tất cả các cửa sổ Notepad rồi Sendkeys "Test":
Mã:
Option Explicit

Declare PtrSafe Function GetDesktopWindow Lib "USER32" () As LongPtr
Declare PtrSafe Function GetWindow Lib "USER32" (ByVal hwnd As LongPtr, ByVal wCmd As Long) As LongPtr
Declare PtrSafe Function GetWindowText Lib "USER32" Alias "GetWindowTextA" (ByVal hwnd As LongPtr, ByVal lpString As String, ByVal cch As LongPtr) As Long

Sub SendKeysToAllNotepadWindows()
    Dim DeskTophWnd As LongPtr
    Dim WindowhWnd As LongPtr
    Dim Buff As String * 255
    Dim hwnd As Long
    Dim CaptionWindowsString As String

    CaptionWindowsString = "Notepad"
    DeskTophWnd = GetDesktopWindow
    WindowhWnd = GetWindow(DeskTophWnd, 5)
    Do While (WindowhWnd <> 0)
        Buff = String$(255, Chr$(0))
        GetWindowText WindowhWnd, Buff, 255
        strtext = String$(100, Chr$(0))
        WindowhWnd = GetWindow(WindowhWnd, 2)

        If InStr(Buff, CaptionWindowsString) <> 0 Then
            AppActivate Buff, True
            DoEvents
            SendKeys "test", True
            DoEvents
        End If
    Loop
End Sub
 

phuonghong1997

Yêu THVBA như điếu đổ
Có cách nào For each để mình duyệt qua từng cửa sổ đang mở không anh.
For each tìm từng cửa sổ thì em cũng không biết. Nhưng nếu tìm Notepad thì anh coi code này :
Mã:
    Dim hWnd As Long
    hWnd = FindWindow("Notepad", vbNullString)
    If hWnd = 0 Then End
    SetForegroundWindow hWnd
    SendKeys "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    SendKeys "{Enter}"
 

NhanSu

SMod
Thành viên BQT
Buff là bộ đệm có kích thước 256 ký tự. Hàm đó trả về chuỗi gồm toàn ký tự 0 để reset lại bộ đệm sau mỗi vòng lặp vì hàm API cần chuỗi kết thúc bằng null.
 

tuhocvba

Administrator
Thành viên BQT
Hai cái này khác gì nhau, chỗ này mình không hiểu, nếu @NhanSu hiểu thì giải đáp giúp mình với:
Mã:
chr(0)
và :
Mã:
chr$(0)
Sao tự nhiên có thêm dấu $ để làm gì vậy ta.
 

NhanSu

SMod
Thành viên BQT
Chr trả về variant còn chr$ trả về string. Nói chung truyền dữ liệu giữa vba và win32 api cần phải đọc thêm về con trỏ và cách lưu trữ của biến vba trong bộ nhớ.
 

tuhocvba

Administrator
Thành viên BQT
Mình chạy thử code này nó đều ra là 8 :
Mã:
MsgBox VarType(Chr$(0))
hoặc:
Mã:
MsgBox VarType(Chr(0))
 

NhanSu

SMod
Thành viên BQT
Vartype của 1 variant chỉ trả về variant (12) khi biến này chứa mảng các variants.
 

tuhocvba

Administrator
Thành viên BQT
Cảm ơn NhanSu.
Vậy câu lệnh đó giờ thay bằng:
Mã:
Buff = cstr(String(255, Chr(0)))
chắc là vẫn ok phải không nhỉ.
 

NhanSu

SMod
Thành viên BQT
Cái này mình cũng không chắc, cần kiểm tra thêm bằng các chương trình đọc nội dung trực tiếp từ bộ nhớ hoặc bạn có thể tìm hiểu thêm về api CopyMemory. Sử dụng hàm này để đọc thông tin vùng nhớ chỉ định sẽ nắm được cách lưu trữ của biến trong bộ nhớ.
 

NhanSu

SMod
Thành viên BQT
@bvtvba thực ra ý mọi người đang bàn luận là nắm rõ cách lưu trữ biến trong bộ nhớ.
Ví dụ khi ta khai báo một biến string bằng lệnh Dim thì biến này có địa chỉ xác định. Giá trị của vùng nhớ tại biến này là địa chỉ của chuỗi unicode kết thúc bằng null. Giá trị của 4 bytes trước chuỗi chứa LEN của chuỗi. Đây là cấu trúc của kiểu BSTR. Làm việc với win api cần nắm những thứ này để tránh lỗi.
 
Top