auto robot
auto robot

Nói đến việc viết auto, có lẽ autoit là cái tên mà có lẽ ai cũng nhắc đến. Ngoài autoit, người ta cũng hay viết auto bằng c++, hoặc c#. Ít thấy ai nhắc đến việc viết auto bằng Java. Bởi vì việc viết auto bằng Java nó khó hơn những ngôn ngữ kia. Java không được thiết kế để viết auto. Java là ngôn ngữ cross platform, trong khi các tool auto đa số chỉ chạy trên Windows và dùng các API của Windows. Nếu bảo người ta viết tool auto trên gtk (linux), thì viết auto bằng c++ nó cũng khó không kém gì với việc viết auto bằng Java đâu. Bài viết này mình chia sẻ một số giải pháp viết auto bằng Java.

Điều khiển chuột.

Java có một class được viết riêng cho việc auto. Đó là java.awt.Robot. Nó được dùng để điều khiển chuột, điều khiển phím và một số việc khác.

Click chuột

Đoạn code sau sẽ thực hiện một click chuột.

Để tiện cho việc sử dụng, mình sẽ refactor lại theo hướng kế thừa lớp Robot.

Tương tự như thế, chúng ta cũng có thể định nghĩa phương thức rightClick như sau:

Double click thì đơn giản là click 2 lần

Click chuột tại vị trí chỉ định

Việc click nó đơn giản vậy đó. Tuy nhiên không phải click vào đâu cũng được, chúng ta cũng cần phải click đúng chỗ. Để cho đúng chỗ, chúng ta cần di chuyển con trỏ chuột đến chỗ cần click. Lớp Robot cung cấp phương thức mouseMove , cho phép chúng ta làm điều đó.

Vì là điều khiển chuột nên sẽ nãy sinh vấn đề “chiếm chuột”. Phương thức mouseMove thay đổi vị trí con trỏ chuột, điều đó làm mình khó chịu. Có lẽ bạn cũng cảm thấy như thế. Để hạn chế sự khó chịu này thì trước khi di chuyển con trỏ chuột, chúng ta lưu tọa độ con trỏ chuột ở vị trí hiện tại (đối tượng java.awt.Point), sau đó mới di chuyển con trỏ đến tọa độ cần click, click rồi di chuyển con trỏ trở về vị trí cũ. Để lấy vị trí con trỏ chuột hiện tại, chúng ta dùng đến class  java.awt.MouseInfo.

Điều khiển bàn phím

Cũng như điều khiển chuột, điều khiển bàn phím, chúng ta cũng dùng lớp Robot.

Gõ một phím

Tương tự như phương thức click, ta có phương thức keyType như sau .

keyCode ở đây là các hằng số java.awt.event.KeyEvent.VK_*.

Gõ một đoạn văn bản cơ bản

Có những lúc chúng ta cần gõ một chuỗi, chứ không phải là từng phím cụ thể. Để làm được điều đó, chúng ta có thể dùng phương thức KeyEvent.getExtendedKeyCodeForChar để lấy keyCode của từng kí tự trong chuỗi rồi truyền keyCode vào phương thức keyType.

Một vấn đề phát sinh là có những keyCode không được chấp nhận bởi Robot, có những kí tự phải dùng kết hợp với phím shift. Còn đối với các kí tự chữ cái, chữ hoa hay chữ thường thì còn phải phụ thuộc vào trạng thái caps lock nữa. Để kết hợp với phím shift. Chúng ta có thể dùng  keyPress(KeyEvent.VK_SHIFT), gõ phím, rồi dùng keyRelease(KeyEvent.VK_SHIFT). Hoặc viết một phương thức để tái sử dụng như bên dưới.

Dùng tổ hợp phím

Để cho tiện sử dụng, mình định nghĩa phương thức keyCombine theo hướng tiếp cận của functional programing. Ví dụ để nhấn tổ hợp phím Control+Shift+Esc, ta gọi  keyCombine(KeyEvent.VK_CONTROL, () -> keyCombine(KeyEvent.VK_SHIFT, () -> keyType(KeyEvent.VK_ESCAPE)));

Gõ một đoạn văn bản cải tiến

Để gõ các kí tự đặc biệt có kết hợp phím shift, chúng ta định nghĩa phương thức getKeyCodeForTyping để lấy keyCode thay thế khi cần. Để xác định có cần kết hợp phím shift hay không, chúng ta định nghĩa phương thức isShiftRequired. Để kiểm tra trạng thái caps lock, chúng ta dùng java.awt.Toolkit. Gộp  getKeyCodeForTyping và  isShiftRequired có thể giúp tối ưu performance một chút (xíu xìu xiu) nhưng có lẽ nó sẽ làm code trở nên khó đọc hơn, nên mình quyết định không gộp.

Lúc này, chúng ta sửa lại phương thức textType như sau:

Dán văn bản

Điều khiển bàn phím thì dùng để nhập các kí tự trên bàn phím. Vậy các kí tự không có trên bàn phím thì thế nào? Có một giải pháp là đưa văn bản dưới dạng java.awt.datatransfer.StringSelection vào clipboard ( java.awt.datatransfer.Clipboard ), sau đó dùng tổ hợp phím Control+V để dán văn bản.

Tuy nhiên phương pháp này chiếm clipboard, nên tùy thuộc vào nhu cầu sử dụng, bạn sẽ cần xác định dùng textType hay textPaste hoặc 1 giải pháp nào đó khác.

Chụp ảnh màn hình

Class Robot cũng cung cấp cho chúng ta phương thức createScreenCapture để chụp ảnh màn hình. Phương thức này đòi hỏi một tham số là một java.awt.Rectangle và trả về một java.awt.image.BufferedImage.

Chụp ảnh toàn màn hình

Để chụp ảnh toàn bộ màn hình, chúng ta cần lấy được kích thước màn hình. Chúng ta dùng Toolkit.getDefaultToolkit().getScreenSize() để lấy kích thước màn hình dưới dạng một java.awt.Dimension. Sau đó khởi tạo một Rectangle để truyền vào phương thức createScreenCapture.

Trường hợp có nhiều màn hình

Trong trường hợp máy tính có nhiều màn hình thì phương thức fullScreenCapture bên trên chỉ chụp màn hình đầu tiên. Để chụp màn hình khác thì bạn có thể thông qua lớp java.awt.GraphicsEnvironment để lấy java.awt.GraphicsDevice tương ứng với màn hình cần chụp. Từ đó lấy được bounds Regtangle của màn hình cần chụp thông qua lời gọi graphicsDevice.getDefaultConfiguration().getBounds(). Cũng bằng cách này, chúng ta cũng có thể chụp ảnh tất cả các màn hình.

Ngoài ra chúng ta cũng có thể chụp ảnh màn hình bằng cách dùng phím print screen keyType(KeyEvent.VK_PRINTSCREEN), để chụp ảnh màn hình lưu vào clipboard, sau đó lấy ảnh từ trong clipboard ra.

Trường hợp chỉ cần 1 pixel

Trong thực tế, đôi lúc chúng ta chỉ cần 1 pixel cụ thể chứ không cần chụp ảnh cả màn hình. Để tiện sử dụng, chúng ta có thể định nghĩa phương thức getPixel như sau.

Từ đó, chúng ta có thể viết một công cụ color picker đơn giản như sau:

Auto một app Java Swing đơn giản

Khi cần viết công cụ auto bằng Java trên một app Java Swing thì có một cách đơn giản đó là viết launcher cho App Java đó. Chúng ta sẽ xem file jar của app đó là một dependency của launcher và tất cả dependencies của app đó cũng là dependencies của launcher. Chúng ta sẽ gọi vào phương thức main của app đó. Để xác định phương thức  main của app nằm ở class nào, chúng ta có thể căn cứ vào mục Main-Class trong file META-INF\MANIFEST.MF nằm trong file jar. Để xác định app có những dependencies nào, chúng ta có thể căn cứ vào mục Class-Path trong file  META-INF\MANIFEST.MF. Hoặc cũng có những khi chúng ta cũng phải căn cứ dựa vào những thứ khác (ví dụ như file laucher khác dạng sh, cmd…).

Ở phần này mình sẽ lấy ví dụ là một Swing app đơn giản, với main class là diep.esc.demo.app.DemoApp và không có bất kì jar dependency nào cả.

demo swing app
demo swing app (ảnh chụp màn hình)

Gọi phương thức main của  DemoApp như bên dưới, chúng ta đã có một launcher đơn giản.

Có launcher rồi, việc tiếp theo là tìm và thao tác với các UI Component.

Tìm một JButton

Giả sử bây giờ chúng ta cần tìm và click 2 lần vào JButton “Click me”. Vậy thì tìm cái JButton “Click me” như thế nào? Một khi chúng ta có reference của đối tượng JFrame thì chúng ta có thể duyệt qua tất cả các Component có mặt trên JFrame đó. Tuy nhiên chúng ta đang code ở launcher, và chúng ta không sửa code của  DemoApp, vậy thì làm sao để có được reference của nó? Lớp java.awt.Window có cung cấp phương thức getWindows, cho phép chúng ta lấy được reference của tất cả các Window được tạo bởi app hiện tại bao gồm cả các JFrame (Đương nhiên rồi vì một JFrame cũng là một Window). Với ý tưởng này, chúng ta có thể định nghĩa phương thức findButtonByText như sau:

Phương thức delay đơn giản

Khi có  findButtonByText rồi, chúng ta có thể gọi  findButtonByText("Click me"). Nhưng khoan, kết quả rất có thể là null. Vì rất có thể khi gọi phương thức này thì cái JFrame kia nó còn chưa được tạo ra. Vậy thì chúng ta hãy đợi 1 chút. Chúng ta có thể đợi bằng phương thức Thread.sleep. Nhưng vì  Thread.sleep. có “throws” một checked Exception. Nó khá là bất tiện. Lớp Robot cũng có phương thức delay làm điều tương tự như không “throws” checked Exception. Tuy nhiên mình cũng không muốn tạo mới một đối tượng Robot trong lúc này. Mình sẽ định nghĩa phương thức delay mới.

Lúc này chúng ta có phương thức main như sau:

Ở đây mình dùng SwingUtilities.invokeLater vì thao tác doClick cập nhật lên UI của app.  Để cho tiện sử dụng, mình dùng  import static javax.swing.SwingUtilities.invokeLater;.

Tìm Component dựa vào text

Ngoài việc tìm JButton, chúng ta cũng có những tình huống tìm những Component khác. Vậy thì chúng ta có thể định nghĩa findComponentByText để sử dụng trong trường hợp tổng quát hơn. Nhưng với  Component, có một vấn đề mới phát sinh, đó là không phải Component nào cũng có phương thức getText như JButton. Để khắc phục vấn đề này, chúng ta sẽ dùng đến Reflection. (Hoặc nếu không muốn dùng Reflection thì có thể liệt kê tất cả các lớp con của Component mà có phương thức getText, rồi kiểm tra class truyền vào có phải là một trong các class đó hoặc là class con của một trong các class đó hay không, kiểm tra instanceof rồi ép kiểu, gọi phương thức getText)

Chúng ta có thể dùng findComponentByText để tìm và click JButton “Submit” như sau:

Nếu bạn chạy thử đoạn code này với DemoApp mà mình đưa ra, thì nó sẽ hiển thị một JDialog. Một  JDialog cũng là một  Window. Vậy câu hỏi đặt ra là một app có thể có nhiều Window (có thể có nhiều JFrame, nhiều JDialog…) thì muốn chỉ tìm cái Component mong muốn trên một Window thôi có được không? Câu trả lời là được, đương nhiên. Phương thức findComponentByText mình định nghĩa bên trên có một phương thức overload nhận tham số  Component root, chúng ta đưa Window vào tham số này thì phương thức sẽ chỉ tìm trên Window mà chúng ta đưa vào thôi.

Tìm Window

Để xác định Window mà chúng ta mong muốn thì cũng tương tự như cách chúng ta làm đối với  findComponentByText nhưng thay getText bằng getTitle và không cần phải đệ quy nữa.

Lúc này chúng ta có thể viết đoạn code click vào nút “Submit” sau đó click vào nút OK trên JDialog có title là “Submit confirm”.

Duyệt qua các Component

Đối với các Component có text cố định và khác nhau, thì chúng ta có thể căn cứ vào text để tìm Component. Vậy những Component có không có text, có text không cố định, text giống nhau thì sao? Thì phải dựa vào những đặc điểm khác nhau để phân biệt thôi. Ví dụ như sự tương quan vị trí của các Component. Trước tiên chúng ta có thể định nghĩa phương thức forEarchComponent như bên dưới.

Có  forEarchComponent, chúng ta có thể kiểm tra hay thao tác với các  Component mong muốn.

Tìm Component dựa vào Predicate

Chúng ta cũng có thể định nghĩa phương thức findComponent với tham số là Predicate thay vì text như phương thức findComponentByText.

Sau khi có findComponent, chúng ta có thể tìm và thay đổi text của các JTextField dựa vào tương quan vị trí so với các Component khác.

Dùng Stream

Các phương thức mình trình bày bên trên chưa đủ linh động. Để cho linh động hơn, mình định nghĩa luôn phương thức getComponents, trả về Stream.

Ý tưởng cho Layout Inspector

Với những thứ được đề cập ở bên trên, chúng ta cũng có thể áp dụng để viết một Layout Inspector đơn giản cho app Swing. Kiểu như:

Tuy nhiên, bài viết này mình sẽ không đào sâu vào việc xây dựng Layout Inspector cho Swing.

Một số vấn đề khác

Auto web

Viết auto bằng Java để làm việc với web, chúng ta có thể nghĩ tới việc nhúng Web Brower vào tool auto. Có một dự án mã nguồn mở hỗ trợ chúng ta trong việc này. Đó là The DJ Project.

Ngoài ra, còn có các thư viện wrapper trình duyệt chromium cho việc sử dụng trên java như java-cef (open source) và JxBrowser (có phí). (Disclamer: Mình chưa từng thử qua 2 món này)

Tuy nhiên, những thứ trên không được thiết kế sẵn cho auto. Có một thứ được thiết kế sẵn cho auto, đó là Selenium. Nói đến Automation test thì có lẽ không ai không biết đến Selenium. Và đương nhiên rồi, Selenium cũng cho phép chúng ta viết auto bằng Java. (Disclamer: Mình cũng chưa từng làm việc với Selenium 😆 )

Dùng Windows APIs

Có lẽ cách nhanh gọn nhất để dùng Windows API trên Java là dùng JNA.

Kết

Bài viết này mình đã chia sẽ những hiểu biết, cũng như kinh nghiệm của mình về vấn đề viết tool auto bằng Java. Thú thật với các bạn, mình mất hơn 3 ngày 3 đêm (có thể nói là 4 ngày 3 đêm) để có thể viết xong bài viết này. Khá là oải. Không biết có còn thiếu sót gì hay không. Nếu có thiếu sót hay bất cứ thắc mắc gì, các bạn cứ bình luận vào bên dưới hoặc inbox m.me/DiepEsc cho mình biết nhé. Cảm ơn các bạn đã theo dõi bài biết dài ơi là dài của mình.

Bây giờ, thử tổng kết xem chúng ta thu được những gì sau bài viết này nào:

Class MyRobot:

Class DemoAuto: