3

I've been wanting to make a sorting algorithm visualizer using python and settled to use the Tkinter library as my way of visualizing the data (if anyone has better libraries to use I'm open to suggestions, I looked into matplotlib but became discouraged). My problem is that as I'm sorting the array, I want to make a swap, show the updated array after the swap, then continue the sorting; but what ends up happening is that the array sorts and then the entire sorted array is updated.

import tkinter as tk
from tkinter import ttk
import random
import time

class SortingVisualizer(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        tk.Tk.wm_title(self, "Sorting Visualizer")
        tk.Tk.wm_minsize(self, width=600, height=500)
        tk.Tk.wm_resizable(self, width=False, height=False)


        self.topFrame = tk.Frame(self)
        self.topFrame.grid(row=0, sticky='w')

        self.sortOptions = ['Select Algorithm','Bubble sort','Quicksort', 'Merge sort']
        self.optionVar = tk.StringVar()
        self.optionDrop = ttk.OptionMenu(self.topFrame, self.optionVar, *self.sortOptions)
        self.optionDrop.config(width=15)
        self.optionDrop.grid(row=0, column=1, sticky='ew')

        self.sortButton = ttk.Button(self.topFrame, text = "Sort", command = lambda: bubbleSort(self))
        self.sortButton.grid(row=0, column=2, sticky='w')
        self.genButton = ttk.Button(self.topFrame, text = "Generate New Array", command = self.newArray)
        self.genButton.grid(row=0, column=0)

        self.generateArray()

    def newArray(self):
        self.sortCanvas.destroy()
        self.generateArray()

    def generateArray(self):
        self.array = []
        self.numOperations = 0
        i = 0
        while i < 15:
            height = random.randint(15, 200)
            self.array.append(height)
            i = i + 1
        self.drawCanvas()

    def drawCanvas(self):
        self.sortCanvas = tk.Canvas(self, width=600, height=450)
        self.sortCanvas.grid(row=1)
        self.sortCanvas.create_line(15, 15, 585, 15)
        label = "Number of Operations: " + str(self.numOperations)
        self.numLabel = tk.Label(self.topFrame, text = label)
        self.numLabel.grid(row=1)

        bar_width = 20
        bar_gap = bar_width + 10
        start_x = 30
        start_y = 15
        for bar_height in self.array:
            x1 = start_x + bar_width
            y1 = start_y + bar_height
            self.sortCanvas.create_rectangle(start_x, start_y, x1, y1*2, fill='green')
            start_x = start_x + bar_gap

    def redrawCanvas(self):
        self.sortCanvas.destroy()
        self.drawCanvas()

def bubbleSort(self):
    n = len(self.array)
    for i in range(n):
        for j in range(0, n-i-1):
            if self.array[j]>self.array[j+1]:
                temp = self.array[j]
                self.array[j] = self.array[j+1]
                self.array[j+1] = temp
                self.numOperations += 1
                self.after(300, self.redrawCanvas)

app = SortingVisualizer()
app.mainloop()

I have also tried app.after(300, self.redrawCanvas) and get the same result

daylenp
  • 41
  • 3
  • Read [use threads to preventing main event loop from “freezing”](https://stackoverflow.com/a/16747734/7414759) – stovfl Oct 09 '19 at 08:12

3 Answers3

1

You can Thread your function bubbleSort. Also it seems to make more sense for bubbleSort to be a method of the class:

import threading, time

class SortingVisualizer(tk.Tk):
    def __init__(self, *args, **kwargs):
        ...

        self.sortButton = ttk.Button(self.topFrame, text = "Sort", command = lambda: threading.Thread(target=self.bubbleSort).start())

        ...

    ...


    def bubbleSort(self):
        n = len(self.array)
        for i in range(n):
            for j in range(0, n-i-1):
                if self.array[j]>self.array[j+1]:
                    temp = self.array[j]
                    self.array[j] = self.array[j+1]
                    self.array[j+1] = temp
                    self.numOperations += 1
                    self.redrawCanvas()
                    time.sleep(0.1)
Henry Yik
  • 22,275
  • 4
  • 18
  • 40
  • Would it be better to not have classes at all for this program then? I was just using bubble sort as a test to try and get the program to work. My plan is to add multiple sorting algorithms in a different file and then import that file to main and call the functions like that. – daylenp Oct 09 '19 at 20:03
  • You can move it outside of the class and remove the `self` arg - it is unnecessary outside of a class. – Henry Yik Oct 10 '19 at 03:11
1

Just an alternative solution besides threading. I used to encounter a problem that by threading the functions, they may access some external devices (I created a GUI to monitor some hardware) at the same time and cause clashes. By using .after(), tkinter will handle the order of the tasks to avoid clashes.

You can redefine your bubbleSort function so that each iterations of the for loop is changed to a recursion by calling the function again.

def bubbleSort(self, i = 1, j = 0):
    n = len(self.array)
    if self.array[j]>self.array[j+1]:
        temp = self.array[j]
        self.array[j] = self.array[j+1]
        self.array[j+1] = temp
        self.numOperations += 1
    j += 1
    if j == n-i-1:
        j = 0
        i += 1
    if i < n:
        self.after(1, lambda: self.bubbleSort(i,j))
Mayan
  • 492
  • 4
  • 11
1

You made a very good first attempt and you were almost there. I have made some changes in your code, most importantly introduced a root object (tk.Tk()) so I can do root.update() in order to redraw to sort_canvas in a new method blip_canvas. To avoid some 'flickering' rather to destroy the canvas each time it is better to delete the 'bar' elements only. Further I took the liberty to change some of the variable names to make it a bit more Pythonic (should make use of underscores rather than capitals) and added the if _name__ == '__main__' statement.

Have a look at the code below.

import tkinter as tk
from tkinter import ttk
import random

class SortingVisualizer:

    def __init__(self):
        self.root = tk.Tk()
        self.root.wm_title("Sorting Visualizer")
        self.root.wm_minsize(width=600, height=500)
        self.root.wm_resizable(width=False, height=False)

        self.top_frame = tk.Frame(self.root)
        self.top_frame.grid(row=0, sticky='w')

        self.sort_options = ['Select Algorithm', 'Bubble sort', 'Quicksort', 'Merge sort']
        self.option_var = tk.StringVar()
        self.option_drop = ttk.OptionMenu(
            self.top_frame, self.option_var, *self.sort_options)
        self.option_drop.config(width=15)
        self.option_drop.grid(row=0, column=1, sticky='ew')

        self.sort_button = ttk.Button(
            self.top_frame, text="Sort", command=self.bubble_sort)
        self.sort_button.grid(row=0, column=2, sticky='w')

        self.gen_button = ttk.Button(
            self.top_frame, text="Generate New Array", command=self.new_array)
        self.gen_button.grid(row=0, column=0)

        self.sort_canvas = tk.Canvas(self.root)
        self.bars = []

    def new_array(self):
        self.generate_array()
        self.blip_canvas()

    def generate_array(self):
        self.array = []
        self.num_operations = 0
        i = 0
        while i < 15:
            height = random.randint(15, 200)
            self.array.append(height)
            i = i + 1

    def draw_canvas(self):
        label = "Number of Operations: " + str(self.num_operations)
        self.num_label = tk.Label(self.top_frame, text=label)
        self.num_label.grid(row=1)

        self.sort_canvas = tk.Canvas(self.root, width=600, height=450)
        self.sort_canvas.grid(row=1)
        self.sort_canvas.create_line(15, 15, 585, 15)

        bar_width = 20
        bar_gap = bar_width + 10
        start_x = 30
        start_y = 15
        self.bars = []
        for bar_height in self.array:
            x1 = start_x + bar_width
            y1 = start_y + bar_height
            self.bars.append(self.sort_canvas.create_rectangle(
                start_x, start_y, x1, y1*2, fill='green'))
            start_x = start_x + bar_gap

    def blip_canvas(self):
        self.sort_canvas.delete(self.bars)
        self.draw_canvas()
        self.root.update()
        self.root.after(200)

    def bubble_sort(self):
        n = len(self.array)
        for i in range(n):
            for j in range(n-i-1):
                if self.array[j] > self.array[j+1]:
                    self.array[j], self.array[j+1] = self.array[j+1], self.array[j]
                    self.num_operations += 1
                    self.blip_canvas()

    def start(self):
        tk.mainloop()


if __name__ == '__main__':
    app = SortingVisualizer()
    app.start()

Note in bubble_sort you do not need the temp variable to swap the values of array[j] and array[j+1]

Rather than to use time.sleep(0.2) to set a delay I have used:

self.root.update()
self.root.after(200)

as suggested in Update button after delay

You can also stick to your original code and just make a few changes.
1) Change the sortButton

self.sortButton = ttk.Button(self.topFrame, text = "Sort", command=self.bubbleSort)

2) Indent the bubbleSort method to be aligned with SortingVisualizer

3) Change method redrawCanvas to:

    def redrawCanvas(self):
        self.sortCanvas.destroy()
        self.drawCanvas()
        self.update()
        self.after(300)

and

4) in bubbleSort make the call to redrawCanvas:

        for j in range(0, n-i-1):
            if self.array[j]>self.array[j+1]:
                temp = self.array[j]
                self.array[j] = self.array[j+1]
                self.array[j+1] = temp
                self.numOperations += 1
                self.redrawCanvas()

et voila, it will work!

Bruno Vermeulen
  • 2,970
  • 2
  • 15
  • 29