Tải bản đầy đủ (.pdf) (17 trang)

Thiết kế giao diện động với Swing ppsx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (293.29 KB, 17 trang )

Thiết kế giao diện động với Swing
Chuyến đi đến các vùng ngoài của Swing API
Peter Seebach, Tác giả, 自由职业者
Tóm tắt: Bộ công cụ Giao diện người dùng (UI) của Swing làm cho nó có thể,
mặc dù không luôn dễ dàng, cập nhật động các giao diện người dùng để đáp lại
các sự kiện hoặc hành động của người dùng. Bài viết này xem xét một số cách phổ
biến mà bạn có thể xây dựng các UI được cập nhật động, xem xét vài cạm bẫy có
thể gặp trên đường đi và một số nguyên tắc để giúp bạn quyết định khi nào đây là
cách tiếp cận thích hợp cho công việc.
Bộ công cụ Swing cung cấp nhiều công cụ để tạo các giao diện người dùng và một
mảng kì lạ về các tùy chọn để thay đổi các giao diện này trong vòng đời của
chương trình. Sử dụng cẩn thận các tính năng này có thể dẫn đến giao diện thích
ứng động theo nhu cầu của người dùng và đơn giản hóa tương tác. Sử dụng không
cẩn thận các tính năng tương tự có thể dẫn đến các chương trình rất khó hiểu hoặc
thậm chí các chương trình hoàn toàn không sử dụng được. Bài viết này giới thiệu
công nghệ và triết lý về các UI động và giúp bạn vượt qua lúc khó khăn khi xây
dựng các UI hiệu quả. Bạn sẽ thay đổi mã nguồn dựa trên ứng dụng chương trình
giới thiệu (demo) của SwingSet2 được đảm bảo với JDK của Sun (xem
Resources); UI của ứng dụng này sử dụng một số đặc tính động và dùng như một
điểm khởi đầu tuyệt vời để hiểu chúng.
Đình chỉ một tiện ích (widget)
Hình thức đơn giản nhất của UI động là làm xám màu của các mục hay nút ấn của
trình đơn chưa sẵn sàng. Việc đình chỉ các widget UI thực hiện theo cùng cách với
tất cả các widget; hàm setEnabled() là một tính năng của lớp Component (Thành
phần). Liệt kê 1 cho thấy mã để đình chỉ một nút:

Liệt kê 1. Đình chỉ một nút

button.setEnabled(false);

Ngay cả hành động đơn giản chuyển sang màu xám một tùy chọn trình đơn không


sử dụng hoặc nút hộp thoại liên quan đến sự thỏa hiệp với người sử dụng. Mặc dù
một nút ấn đã chuyển sang màu xám ngay lập tức thông báo cho người dùng rằng
một hành động cụ thể không thể thực hiện được, những nó không cho họ biết tại
sao. Điều này có thể là một vấn đề với người dùng, người có thể không hiểu lý do
(xem Các nguyên tắc chung).
Thật dễ dàng, khi bạn có thể nhìn thấy. Câu hỏi là khi nào bạn nên kích hoạt hay
đình chỉ một nút. Một quyết định thiết kế chung là đình chỉ một nút khi nó không
thích hợp. Ví dụ, nhiều chương trình đình chỉ nút Save (và mục trình đơn tương
ứng bất kỳ) khi một tệp đã không được thay đổi kể từ lần lưu nó cuối cùng.
Lời cảnh báo chủ yếu cho việc đình chỉ các nút là nhớ kích hoạt lại chúng ở thời
điểm thích hợp. Ví dụ, nếu có một bước xác nhận giữa việc nhấn vào một nút và
hoàn thành hành động của nó, nút đó phải được kích hoạt ngay cả khi việc xác
nhận không thành công.


Điều chỉnh các phạm vi
Đôi khi một ứng dụng cần điều chỉnh phạm vi của một số widget, như là một
Spinner (quay tròn) hoặc Slider (thanh trượt ), một cách động. Điều này có thể
phức tạp hơn nó mong đợi. Các Slider, nói cụ thể, có các tính năng thứ cấp các
dấu thời gian, khoảng cách dấu thời gian và các nhãn mà chúng có thể cần phải
được điều chỉnh cùng với phạm vi đó để tránh bị lỗi nặng.
Chương trình giới thiệu (demo) SwingSet2 không trực tiếp làm bất kỳ cái gì về
điều này, do đó bạn sẽ thay đổi nó bằng cách gắn một ChangeListener cho một
thanh trượt để có thể thay đổi thanh trượt khác. Nhập lớp SliderChangeListener
mới, chỉ ra trong Liệt kê 2:

Liệt kê 2. Thay đổi một phạm vi của thanh trượt

class SliderChangeListener implements ChangeListener {
JSlider h;


SliderChangeListener(JSlider h) {
this.h = h;
}

public void stateChanged(ChangeEvent e) {
JSlider js = (JSlider) e.getSource();
int i = js.getValue();
h.setMaximum(i);
h.repaint();
}
}

Khi thanh trượt ngang thứ ba được tạo ra (một thanh trượt trong bản demo ban đầu
có dấu đánh dấu tất cả các đơn vị và ghi nhãn tại 5, 10 và 11), một
SliderChangeListener mới cũng được tạo ra, chuyển qua thanh trượt đó như là đối
số hàm tạo (constructor). Khi thanh trượt dọc thứ ba (có phạm vi 0 đến100) được
tạo ra, SliderChangeListener mới được thêm vào nó như là một người nghe
(listener) thay đổi. Điều này làm việc gần như mong đợi: Điều chỉnh thanh trượt
dọc thay đổi phạm vi của thanh trượt ngang.
Thật không may, các dấu và các nhãn cũng chẳng làm việc tốt. Các nhãn có tối đa
năm dấu làm việc tốt miễn là phạm vi này không quá lớn, nhưng nhãn phụ tại 11
nhanh chóng là vấn đề về tính sử dụng, như trong Hình 1:

Hình 1. Các nhãn chạy cùng nhau

Cập nhật các dấu và các nhãn
Giải pháp rõ ràng sẽ đơn giản là đặt khoảng cách đánh dấu trên thanh trượt ngang,
bất cứ khi nào giá trị tối đa của nó được cập nhật, như thể hiện trong Liệt kê 3:


Liệt kê 3. Thiết lập khoảng cách đánh dấu

// DOES NOT WORK
int tickMajor, tickMinor;
tickMajor = (i > 5) ? (i / 5) : 1;
tickMinor = (tickMajor > 2) ? (tickMajor / 2) : tickMajor;
h.setMajorTickSpacing(tickMajor);
h.setMinorTickSpacing(tickMinor);
h.repaint();

Liệt kê 3 đúng theo như nó thực hiện, nhưng nó không dẫn đến sự thay đổi nào
cho các nhãn được vẽ trên màn hình. Bạn phải thiết lập các nhãn một cách riêng
biệt, sử dụng setLabelTable(). Thêm một dòng nữa để sửa chữa nó:
h.setLabelTable(h.createStandardLabels(tickMajor));

Điều này vẫn còn để mặc cho bạn với các nhãn lẻ tại 11 đã được thiết lập ban đầu.
Tất nhiên, mục đích này là phải có một nhãn luôn ở đầu mút phải của thanh trượt.
Bạn có thể làm việc này bằng cách loại bỏ nhãn cũ (trước khi thiết lập giá trị tối đa
mới) và sau đó thêm một nhãn mới. Mã này hầu như làm việc:

Liệt kê 4. Thay thế các nhãn

public void stateChanged(ChangeEvent e) {
JSlider js = (JSlider) e.getSource();
int i = js.getValue();

// clear old label for top value
h.getLabelTable().remove(h.getMaximum());

h.setMaximum(i);


int tickMajor, tickMinor;
tickMajor = (i > 5) ? (i / 5) : 1;
tickMinor = (tickMajor > 2) ? (tickMajor / 2) : tickMajor;
h.setMajorTickSpacing(tickMajor);
h.setMinorTickSpacing(tickMinor);
h.setLabelTable(h.createStandardLabels(tickMajor));
h.getLabelTable().put(new Integer(i),
new JLabel(new Integer(i).toString(), JLabel.CENTER));
h.repaint();
}

Nếu tôi đã nói với bạn một lần, thì tôi đã nói với bạn hai lần
Bởi hầu như tôi muốn nói là, mặc dù các mã trong Liệt kê 4 loại bỏ nhãn ở 11, nó
không gắn nhãn mới tại i; thay vào đó, bạn chỉ thấy các nhãn tại các khoảng
tickMajor. Giải pháp này ban đầu hơi gây sốc:

Liệt kê 5. Thúc đẩy cập nhật hiển thị

h.setLabelTable(h.getLabelTable());

Trên thực tế hoạt động có vẻ vô nghĩa này có một tác động đáng kể. Các nhãn cho
một thanh trượt được tạo ra bất cứ khi nào bảng nhãn được thiết lập. Không có
cuộc gọi lại đặc biệt nào đối với các thay đổi trên bảng, do đó các giá trị mới được
bổ sung vào bảng không nhất thiết phải có hiệu quả; không có hoạt động nào rõ
ràng trong Liệt kê 5 có tác dụng phụ để cho Swing biết nó phải cập nhật hiển thị.
(Vì sợ bạn nghĩ rằng tôi đã tự phát minh điều này, hãy chú ý rằng mã gốc
SwingSet có lời gọi như vậy).
Điều này chỉ có một vấn đề. Mong muốn rất hợp lý để đảm bảo rằng một nhãn
xuất hiện ở cuối thanh trượt đôi khi đặt hai nhãn ngay liền kề với nhau, hoặc thậm

chí còn chồng lên nhau, như trong Hình 2:

Hình 2. Xếp chồng các nhãn ở cuối thanh trượt

Có thể có một số giải pháp cho vấn đề này. Một là viết mã riêng của bạn để điền
các giá trị vào bảng nhãn và dừng trình tự trước, để cho nhãn cuối cùng trong trình
tự đó được phân tách một chút khỏi đầu mút của thanh trượt này. Tôi sẽ để lại
phần này như là một bài tập cho bạn.


Cập nhật các trình đơn
Trong nhiều trường hợp, để hạn chế các thay đổi trình đơn để cho phép và đình chỉ
các mục trình đơn là hoàn toàn thực tế. Cách tiếp cận này tùy thuộc vào cảnh báo
chung được áp dụng để đình chỉ các mục: Tránh bỏ quên chương trình của bạn
trong trạng thái không sử dụng được do vô tình đình chỉ các mục chủ yếu.
Cũng có thể thêm hoặc xoá các mục trình đơn hoặc các trình đơn con. Thật không
dễ dàng để thay đổi một JMenuBar; không có giao diện nào để loại bỏ hoặc thay
thế các trình đơn riêng biệt khỏi thanh này. Nếu bạn muốn thay đổi một thanh (để
khỏi thêm các trình đơn mới vào đầu mút phải của nó), bạn cần phải tạo một thanh
mới và dùng nó thay thế cho một thanh cũ.
Các thay đổi cho các trình đơn riêng biệt có hiệu lực ngay lập tức; bạn không cần
phải xây dựng một trình đơn trước khi gắn nó vào một thanh hoặc trình đơn khác.
Khi bạn cần phải thay đổi lựa chọn của mình về các tùy chọn trình đơn, cách dễ
nhất là thay đổi một trình đơn cụ thể. Tuy nhiên, bạn có thể muốn thêm vào và loại
bỏ toàn bộ các trình đơn và để làm như vậy chẳng có khó khăn đặc biệt nào. Liệt
kê 6 cho thấy một ví dụ đơn giản của một phương thức chèn một trình đơn vào
thanh trình đơn trước một chỉ mục đã cho. Ví dụ này giả định rằng JMenuBar
được thay thế được gắn vào một đối tượng JFrame, nhưng bất cứ điều gì cho phép
bạn nhận được và thiết lập các thanh menu sẽ làm việc theo một cách giống như
vậy:


Liệt kê 6. Chèn một trình đơn vào thanh trình đơn

public void insertMenu(JFrame frame, JMenu menu, int index) {
JMenuBar newBar = new JMenuBar();
JMenuBar oldBar = frame.getJMenuBar();
MenuElement[] oldMenus = oldBar.getSubElements();
int count = oldBar.getMenuCount();
int i;

for (i = 0; i < count; ++i) {
if (i == index)
newBar.add(menu);
newBar.add((JMenu) oldMenus[i]);
}
frame.setJMenuBar(newBar);
}

Mã này không phải là những gì mà tôi đã nhắm trước tiên; phiên bản cuối cùng
này, được sửa chữa cẩn thận để nó hoạt động, phản ánh một vấn đề khó khăn về
các thói quen thú vị. Ban đầu có thể có vẻ như cách rõ ràng để thực hiện điều này
sẽ là sử dụng getComponentAtIndex(), nhưng điều đó đã bị phản đối. May mắn
thay, giao diện getSubElements() là đủ tốt. Khuôn mẫu với JMenu cho
newBar.add() có lẽ an toàn, nhưng tôi không thích nó. Giao diện getSubElements()
hoạt động trên trình đơn, không chỉ là các thanh trình đơn; các trình đơn có thể có
các phần tử con theo một số kiểu, nhưng các JMenu là các phần tử duy nhất mà
bạn có thể thêm vào JMenuBar. Vì vậy bạn phải tạo khuôn mẫu phần tử đó cho
JMenu để vượt qua nó đến phương thức JMenuBar.add(). Thật không may là nếu
bản sửa đổi API trong tương lai cho phép bạn thêm các phần tử của các kiểu khác
với JMenu cho một JMenuBar, thì nó sẽ không còn cần thiết nữa, hoặc thậm chí an

toàn, để tạo khuôn mẫu các phần tử trả về tới JMenu.
Mã trong Liệt kê 6 phản ánh một điều khác ngoài thói quen giao diện khôn ngoan;
vấn đề trình đơn phải được lưu trữ trước. Khi các trình đơn được thêm vào thanh
mới, chúng được tách khỏi thanh cũ. Mã trong Liệt kê 7, mặc dù nó có vẻ tương
tự, nhưng không làm việc; vòng lặp chấm dứt sớm:

Liệt kê 7. Vòng lặp kết thúc quá sớm

// DOES NOT WORK
for (i = 0; i < oldBar.getMenuCount(); ++i) {
if (i == index)
newBar.add(menu);
newBar.add((JMenu) oldMenus[i]);
}

Những người dùng được hưởng lợi từ tính nhất quán trong một giao diện; một
trình đơn đã cho luôn ở cùng chỗ. Để thuận tiện cho người dùng, hãy cố gắng giữ
cho các trình đơn có thể thay đổi về đầu mút phải của danh sách các trình đơn, còn
với các trình đơn không thay đổi được đặt ở những vị trí cố định phía bên trái.
Tương tự như vậy, khi luôn luôn có thể, hãy giữ cho các mục ở cùng một vị trí
trong một trình đơn. Một mục trình đơn chuyển sang màu xám ít gây rối cho người
dùng hơn một mục đến và đi, bởi vì các mục khác trên trình đơn đang không được
di chuyển theo nhiều hướng.
Vòng lặp trong Liệt kê 7 sao chép chỉ một nửa các mục đó. Ví dụ, nếu bốn mục có
trên thanh trình đơn để bắt đầu, nó sao chép hai mục đầu tiên. Sau khi sao chép
mục thứ nhất, i là 1 và getMenuCount() trả về 3; sau khi sao chép mục thứ hai, i là
2 và getMenuCount() trả về 2, do đó, vòng lặp kết thúc. Tôi không thể tìm thấy bất
kỳ tài liệu hướng dẫn về "tính năng" nào, theo đó việc thêm một trình đơn vào một
thanh loại bỏ nó khỏi thanh khác, vì vậy nó có thể không phải do cố ý. Vẫn còn,
thật dễ dàng đủ để tiến gần đến.

Loại bỏ một trình đơn từ một thanh trình đơn dễ dàng hơn một chút; chỉ cần sao
chép tất cả các trình đơn khác hơn từ thanh cũ đến thanh mới, và bạn đã hoàn tất.
Thật dễ dàng!
Nếu giao diện của bạn sử dụng rất nhiều thông tin cập nhật trình đơn động, có lẽ
tốt hơn là tạo ra một bộ các thanh trình đơn và chuyển giữa chúng, thay vì cập nhật
chúng lúc đang hoạt động trong tất cả thời gian. Tuy nhiên, nếu bạn thay đổi nhiều
trình đơn, bạn cũng có thể khiến người dùng của bạn theo cách hoàn toàn điên rồ.
Lỗi viết: Trong quá trình soạn thảo bài viết này, tôi đã không chú ý đến danh sách
các phương thức kế thừa của lớp JMenuBar. Trong thực tế, nó có cả phương pháp
remove (loại bỏ) và add (bổ sung) sẵn sàng để loại bỏ hay chèn một chỉ số đặc
biệt. Bài học nữa là: hãy kiểm tra các phương thức kế thừa, không chỉ các phương
thức lớp cụ thể.


Thay đổi lại kích thước cửa sổ
Một điều rất may mắn là trong đa số trường hợp, việc thay đổi kích thước cửa sổ
xảy ra tự động. Nhưng bạn cần phải tính đến một vài tác động về việc thay đổi
kích thước. Các thanh nút ấn, các thanh trình đơn và các tính năng tương tự có thể
trở thành vấn đề trong một cửa sổ rất nhỏ. Các bảng (panel) đồ họa mà chương
trình của bạn quản lý tự nó cần phải đáp ứng các sự kiện thay đổi kích cỡ. Hãy để
cho Swing xử lý đóng gói các phần tử UI, nhưng chú ý đến kích thước của các
phần tử; không chỉ nhận được kích thước một lần và tiếp tục sử dụng các giá trị
đó.
Khôn khéo hơn, một số quyết định thiết kế, như tần suất của các dấu trên các
thanh trượt, có thể được cập nhật hợp lý để đáp ứng với các sự kiện thay đổi kích
thước cửa sổ. Độ rộng của một thanh trượt 100 pixel (điểm ảnh) không thể có
nhiều nhãn có thể đọc như độ rộng một thanh trượt 400 điểm ảnh. Bạn có thể
muốn lấy thêm một số các UI của bạn bằng cách thêm toàn bộ các tính năng tiện
lợi mới về hiển thị lớn hơn.
Tuy nhiên, phần lớn, bạn có thể bỏ qua việc thay đổi kích thước cửa sổ. Những gì

bạn không nên làm là ngăn chặn hay ghi đè lên nó không cần thiết. Sự thuận tiện
bên lề so với mã thể hiện của bạn không là điều cần thiết. Một kích thước cửa sổ
tối thiểu có thể là hợp lý, nhưng hãy cho phép mọi người tạo ra các cửa sổ lớn như
họ muốn.


Nguyên tắc chung
Bộ công cụ Swing cung cấp rất nhiều tính linh hoạt cho thiết kế giao diện người
dùng. Được sử dụng cẩn thận, tùy chọn về cập nhật một giao diện trong lúc đang
hoạt động có thể đơn giản hóa giao diện đó đáng kể; việc trình bày một trình đơn
chỉ khi tùy chọn của nó áp dụng, ví dụ, có thể dễ dàng hơn cho người sử dụng.
Thật không may, một số tính năng API làm cho cách tiếp cận này có khả năng là
một thói quen nhỏ và những tác dụng phụ và các tương tác không phải lúc nào
cũng được tạo tài liệu tốt như bạn mong muốn. Nếu bạn có ý tưởng về giao diện
động, thì hãy sẵn sàng dành chút thời gian nữa cho việc gỡ rối. Bạn cũng có thể
đang hoạt động ngoài các góc của thư viện Swing và thấy mình cần phải tiến gần
đến nắm bắt các hành vi và/hoặc các lỗi.
Đừng để việc thiếu sự thực hiện rõ ràng ngăn cản bạn. Ví dụ JMenuBar của bài
viết này cho thấy, ngay cả khi không có sự hỗ trợ cho một nhiệm vụ trong API,
bạn vẫn có thể có khả năng tự mình thực hiện nó, mặc dù có một chút gián tiếp.
Đừng quá nhiệt tình. Các UI động lúc tốt nhất của chúng là khi chúng làm cho các
hạn chế vốn có rõ ràng hơn cho người dùng. Lý tưởng, một người dùng có thể
thậm chí không nhận thấy rằng một giao diện đang thay đổi. Nếu thời gian duy
nhất mà chúng có thể sử dụng trình đơn Object (Đối tượng) của chương trình là
khi chúng có một đối tượng được chọn, thì chúng sẽ không nhớ rằng trình đơn đó
không có phần thời gian còn lại ở đó.
Mặt khác, nếu có một khả năng tồn tại là người dùng không thể đoán được lí do
không có sẵn một tùy chọn, tốt hơn là để người dùng thử một hành động và nhận
được thông báo lỗi thông tin. Điều này đặc biệt quan trọng đối với một số hành
động. Nếu tùy chọn save bị đình chỉ, điều đó không giúp gì nhiều, khi tôi muốn

lưu dữ liệu của mình. Chương trình này rất có thể nghĩ rằng nó đã được lưu lại,
nhưng tại sao không cho tôi lưu nó! Và nếu có một lý do cụ thể tại sao tôi không
thể lưu tệp này, có lẽ tôi muốn biết đó là gì.
Thiết kế giao diện, mặc dù đã nhiều năm nghiên cứu, vẫn còn là một lĩnh vực trẻ
theo nhiều cách khác nhau. Hãy thử nghiệm một chút. Các thay đổi động cho các
UI có thể là một tính năng tuyệt vời làm cho chúng rõ ràng hơn, đơn giản hơn, và
phản ứng nhanh hơn. Việc thêm các tính năng động yêu cầu bất cứ thứ gì từ công
việc cần một vài phút đến một nhiệm vụ cần thời gian đáng kể.

Mục lục

 Đình chỉ một tiện ích (widget)
 Điều chỉnh các phạm vi
 Cập nhật các trình đơn
 Thay đổi lại kích thước cửa sổ
 Nguyên tắc chung

×