Advanced zyLab workspaces can use GUIs in various languages, like Swing and JavaFX in Java, or Tkinter and PyGame in Python. The desktop tab of these workspaces allow the visual output of those GUIs to display.
Some languages have GUI specific workspace setups, like Python with Turtle above. Here, the desktop tab is renamed "Turtle". These GUI specific setups default to display the visual output on Run, as do C++ with Turtle, Java with Turtle, and Java Karel. Other GUI packages can also be used.
When using a GUI with a language that doesn't default to the desktop tab, we recommended changing the workspace settings to display the desktop tab by default. Open the workspace settings in the bottom left, then scroll down, and select desktop from the dropdown menu for "Open tab on start..."
Note: The console tab will still display if there are errors, but it will not display if the Console is waiting for input. Keep this in mind if a GUI requires input before anything will display in the desktop tab.
Autograding
Output tests cannot access the GUI directly, so unit tests need to be used when autograding GUI projects. Adding a headless environment to your unit test allows it to access the GUI. For example, the java unit test below attempts to enable a headless terminal by sending an HTTP post request locally. It first creates an HttpClient
and prepares a specific JSON payload. An HttpRequest
is then built with the target URI, a header, and the JSON body. If an exception occurs, an error message is written to testFeedback
, the stack trace is printed for debugging, and the method returns false. If no errors occur, it returns true, indicating that the feature was successfully enabled.
import java.io.PrintWriter;
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.*;
public class zyLabsUnitTest {
public boolean passed(PrintWriter testFeedback) {
try {
HttpClient client = HttpClient.newHttpClient();
String json = "{\"name\":\"headless-terminal\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:30025/v1/sys/enable-feature"))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString()
);
System.out.println("Response: " + response.body());
} catch (Exception e) {
testFeedback.write("Failed to enable feature: " + e.getMessage());
e.printStackTrace();
return false;
}
return true;
}
}
Java
This JavaFX example project creates a digital clock in the desktop tab.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class DigitalClock extends Application {
private Label timeLabel = new Label();
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
public void start(Stage primaryStage) {
updateTime();
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> updateTime()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
StackPane root = new StackPane(timeLabel);
Scene scene = new Scene(root, 300, 200);
primaryStage.setTitle("Digital Clock");
primaryStage.setScene(scene);
primaryStage.show();
}
private void updateTime() {
timeLabel.setText(LocalTime.now().format(formatter));
}
public static void main(String[] args) {
launch(args);
}
}
And this Swing project example creates a GUI calculator.
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class Calculator extends JFrame {
private JTextField display;
private double result = 0;
private String operator = "=";
private boolean start = true;
public Calculator() {
setTitle("Calculator");
setSize(250, 300);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
display = new JTextField("0", 15);
display.setHorizontalAlignment(JTextField.RIGHT);
add(display, BorderLayout.NORTH);
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(4, 4));
String[] buttons = { "7", "8", "9", "/", "4", "5", "6", "*", "1", "2", "3", "-", "0", "C", "=", "+" };
for (String b : buttons) {
JButton button = new JButton(b);
button.addActionListener(new ButtonClickListener());
panel.add(button);
}
add(panel, BorderLayout.CENTER);
}
private class ButtonClickListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
if ("0123456789".contains(command)) {
if (start) {
display.setText(command);
} else {
display.setText(display.getText() + command);
}
start = false;
} else {
calculate(Double.parseDouble(display.getText()));
operator = command;
start = true;
}
}
private void calculate(double number) {
switch (operator) {
case "+" -> result += number;
case "-" -> result -= number;
case "*" -> result *= number;
case "/" -> result /= number;
case "=" -> result = number;
}
display.setText("" + result);
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new Calculator().setVisible(true));
}
}
Autograding
Here's an example Java (zyLabs) unit test that enables the headless environment to check for a specific button in our calculator example.
import java.io.PrintWriter;
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.*;
import javax.swing.*;
import java.awt.*;
public class zyLabsUnitTest {
public boolean passed(PrintWriter testFeedback) {
boolean testPassed = false;
// Enable headless
try {
HttpClient client = HttpClient.newHttpClient();
String json = "{\"name\":\"headless-terminal\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:30025/v1/sys/enable-feature"))
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Response: " + response.body());
} catch (Exception e) {
testFeedback.write("Failed to enable feature: " + e.getMessage());
e.printStackTrace();
return false;
}
// Create GUI
try {
// Create the frame
Calculator frame = new Calculator();
// Check if the frame is created
if (frame != null) {
// Find the "7" button by its label
JButton button7 = null;
for (Component comp : frame.getContentPane().getComponents()) {
if (comp instanceof JPanel) {
for (Component panelComp : ((JPanel) comp).getComponents()) {
if (panelComp instanceof JButton) {
JButton button = (JButton) panelComp;
if ("7".equals(button.getText())) {
button7 = button;
break;
}
}
}
}
}
// Ensure the "7" button is found
if (button7 != null) {
button7.doClick();
// Get the display field correctly
JTextField displayField = (JTextField) frame.getContentPane().getComponent(0);
// Check if display is updated
String displayText = displayField.getText();
if ("7".equals(displayText)) {
testPassed = true;
} else {
testFeedback.write("Display did not show the expected result after pressing 7.");
}
} else {
testFeedback.write("Button '7' not found.");
}
} else {
testFeedback.write("GUI frame was not created.");
}
} catch (Exception e) {
testFeedback.write("Exception occurred: " + e.getMessage());
e.printStackTrace();
}
return testPassed;
}
}
Python
This tkinter example project uses a GUI to create a tasklist.
import tkinter as tk
from tkinter import messagebox
def add_task():
task = entry.get()
if task:
listbox.insert(tk.END, task)
entry.delete(0, tk.END)
def remove_task():
try:
index = listbox.curselection()[0]
listbox.delete(index)
except IndexError:
messagebox.showwarning("Warning", "No task selected!")
root = tk.Tk()
root.title("To-Do List")
entry = tk.Entry(root, width=40)
entry.pack()
tk.Button(root, text="Add Task", command=add_task).pack()
tk.Button(root, text="Remove Task", command=remove_task).pack()
listbox = tk.Listbox(root, width=50)
listbox.pack()
root.mainloop()
And this PyGame example project has a ball bouncing around the frame.
import pygame
pygame.init()
# Set up display
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
ball = pygame.Rect(WIDTH // 2, HEIGHT // 2, 20, 20)
ball_speed = [4, 4]
BLACK = (0, 0, 0)
RED = (255, 0, 0)
clock = pygame.time.Clock()
running = True
while running:
screen.fill(BLACK)
ball.x += ball_speed[0]
ball.y += ball_speed[1]
if ball.left <= 0 or ball.right >= WIDTH:
ball_speed[0] = -ball_speed[0]
if ball.top <= 0 or ball.bottom >= HEIGHT:
ball_speed[1] = -ball_speed[1]
pygame.draw.ellipse(screen, RED, ball)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.display.flip()
clock.tick(60) # Limit FPS
pygame.quit()
Autograding
Here's an example Python 3 (zyLabs) unit test that enables the headless environment to check that a new task can be created.
import tkinter as tk
import requests
import unittest.mock as mock
# Enable headless environment
requests.post('http://localhost:30025/v1/sys/enable-feature', json={'name': 'headless-terminal'})
# Prevent mainloop() from timing out test
with mock.patch.object(tk.Tk, "mainloop", return_value=None):
from main import add_task, entry, listbox, root
def test_passed(test_feedback):
root.update_idletasks()
# Simulate entering a task
test_task = "Buy groceries"
entry.insert(0, test_task)
# Call the function to add the task
add_task()
# Ensure the GUI updates after the action
root.update_idletasks()
# Check if the task was added
if listbox.size() > 0 and listbox.get(tk.END) == test_task:
root.destroy() # Ensure the GUI properly exits
return True
else:
test_feedback.write("Task was not added to the listbox.")
root.destroy()
return False