Python Forum
How should I build this app?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How should I build this app?
#1
I made a really useful script that I would like to improve. It essentially keeps track of timeframes of a stock market chart and makes an overall tally. I have made it save the tally and so when I close and re-open it, it goes back to the last configuation (very important).

However, I would like to create several tabs so I can keep track of different charts (SPX, OIL, Bonds etc) and have them all be saved and automatically resort back to their last configuation each time it opens. I am having trouble doing this, so I decided to completely rebuild from scratch.

My question is, how would you go about doing this? Would tabs be a good solution, or should I simply have a menu that takes me to the various chart tallies? Or would you recommend another solution?

Another problem I am having is that the script is very long and cumbersome. I think it would be better if I broke it down into several modules. So if you have any advice in doing so, I am all ears.

The current code is as follows:

import tkinter as tk
import json
import os

class TallyCounterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Tally Counter")
        self.root.overrideredirect(True)  # Remove the window's title bar
        self.root.attributes("-topmost", True)

        # Initialize the total value
        self.total = 0

        # List of timelines and their toggle values
        self.timelines = [
            "Week", "3 Day", "1 Day", "12 Hour", "8 Hour", "6 Hour", 
            "4 Hour", "3 Hour", "2 Hour", "1 Hour", "45 Min", "30 Min", 
            "15 Min", "10 Min", "5 Min"
        ]
        # Store the current value for each timeline (starts at 0)
        self.timeline_values = {timeline: 0 for timeline in self.timelines}
        self.button_states = {timeline: {"plus": None, "minus": None, "zero": None} for timeline in self.timelines}

        # Dictionary to hold Entry, Stop Loss, and Take Profit for each timeframe
        self.fields = {
            timeline: {
                "entry_value": tk.DoubleVar(value=0.0),
                "stop_loss_value": tk.DoubleVar(value=0.0),
                "take_profit_value": tk.DoubleVar(value=0.0),
                "toggle_state": tk.StringVar(value="✖")  # Initial toggle state for tick/cross
            }
            for timeline in self.timelines
        }

        # Load previous configuration if available
        self.load_config()

        # Bind mouse events to enable window dragging, but only on non-interactive areas
        self.root.bind("<Button-1>", self.start_move)
        self.root.bind("<B1-Motion>", self.do_move)

        # Create label to display total
        self.total_label = tk.Label(self.root, text=f"Total: {self.total}", font=("Arial", 24))
        self.total_label.grid(row=0, column=0, columnspan=8, pady=20)

        # Add column labels for Entry, Stop Loss, and Take Profit
        self.add_column_labels()

        # Create buttons and entry fields for each timeline
        self.create_timeline_buttons()

        # Create Reset button
        self.reset_button = tk.Button(self.root, text="Reset", font=("Arial", 18), command=self.reset, width=10)
        self.reset_button.grid(row=len(self.timelines) + 5, column=0, columnspan=8, pady=20)

        # Create close button in the top right
        self.close_button = tk.Button(self.root, text="X", font=("Arial", 18), command=self.on_close, fg="red", width=5)
        self.close_button.place(relx=1.0, rely=0.0, anchor="ne")
        

    def add_column_labels(self):
        """Add labels for Entry, Stop Loss, and Take Profit columns."""
        labels = ["Entry", "Stop Loss", "Take Profit", "✔/✖"]
        for i, label_text in enumerate(labels):
            label = tk.Label(self.root, text=label_text, font=("Arial", 14))
            label.grid(row=1, column=4 + i, padx=5, pady=5)

    def create_timeline_buttons(self):
        """Create buttons and entry fields for each timeline."""
        for i, timeline in enumerate(self.timelines):
            # Create label for the timeline
            label = tk.Label(self.root, text=timeline, font=("Arial", 14), anchor="w", width=10)
            label.grid(row=i+2, column=0, padx=10, pady=5)

            # Create +1, -1, and 0 buttons
            plus_button = tk.Button(self.root, text="+1", font=("Arial", 12), command=lambda t=timeline: self.modify_value(t, 1), width=5, bg="lightgray")
            plus_button.grid(row=i+2, column=1, padx=5, pady=5)

            minus_button = tk.Button(self.root, text="-1", font=("Arial", 12), command=lambda t=timeline: self.modify_value(t, -1), width=5, bg="lightgray")
            minus_button.grid(row=i+2, column=2, padx=5, pady=5)

            zero_button = tk.Button(self.root, text="0", font=("Arial", 12), command=lambda t=timeline: self.reset_value(t), width=5, bg="lightgray")
            zero_button.grid(row=i+2, column=3, padx=5, pady=5)

            # Store references to the buttons
            self.button_states[timeline]["plus"] = plus_button
            self.button_states[timeline]["minus"] = minus_button
            self.button_states[timeline]["zero"] = zero_button

            # Restore the previous button state from loaded config
            self.update_button_colors(timeline)

            # Create entry fields for Entry, Stop Loss, and Take Profit next to the buttons
            self.create_entry_fields(timeline, i+2)

            # Create tick/cross toggle button
            toggle_button = tk.Button(self.root, textvariable=self.fields[timeline]["toggle_state"], font=("Arial", 12), width=5, command=lambda t=timeline: self.toggle_state(t))
            toggle_button.grid(row=i+2, column=7, padx=5, pady=5)

            # Disable drag for interactive elements (buttons and entry fields)
            plus_button.bind("<Button-1>", self.disable_move)
            minus_button.bind("<Button-1>", self.disable_move)
            zero_button.bind("<Button-1>", self.disable_move)
            toggle_button.bind("<Button-1>", self.disable_move)

    def create_entry_fields(self, timeline, row):
        """Create entry fields for Entry, Stop Loss, and Take Profit for each timeline."""
        variables = [
            self.fields[timeline]["entry_value"],
            self.fields[timeline]["stop_loss_value"],
            self.fields[timeline]["take_profit_value"]
        ]

        for i, variable in enumerate(variables):
            entry = tk.Entry(self.root, textvariable=variable, font=("Arial", 12), width=8)
            entry.grid(row=row, column=4 + i, padx=5, pady=5)

            # Disable drag for the entry fields
            entry.bind("<Button-1>", self.disable_move)

    def toggle_state(self, timeline):
        """Toggle between tick (✔) and cross (✖) for the toggle button."""
        current_state = self.fields[timeline]["toggle_state"].get()
        new_state = "✔" if current_state == "✖" else "✖"
        self.fields[timeline]["toggle_state"].set(new_state)

    def modify_value(self, timeline, change):
        """Modify the timeline value by +1 or -1 and update the total."""
        current_value = self.timeline_values[timeline]

        if change == 1:  # User pressed +1
            if current_value == 0:  # From 0 to +1
                self.total += 1
            elif current_value == -1:  # From -1 to +1
                self.total += 2  # Remove -1, then add +1
            # Set the new value to +1
            self.timeline_values[timeline] = 1
            self.button_states[timeline]["minus"].config(bg="lightgray")  # Unhighlight -1 button
            self.button_states[timeline]["plus"].config(bg="green")  # Highlight +1 button
            self.button_states[timeline]["zero"].config(bg="lightgray")  # Reset 0 button color

        elif change == -1:  # User pressed -1
            if current_value == 0:  # From 0 to -1
                self.total -= 1
            elif current_value == 1:  # From +1 to -1
                self.total -= 2  # Remove +1, then subtract -1
            # Set the new value to -1
            self.timeline_values[timeline] = -1
            self.button_states[timeline]["plus"].config(bg="lightgray")  # Unhighlight +1 button
            self.button_states[timeline]["minus"].config(bg="red")  # Highlight -1 button
            self.button_states[timeline]["zero"].config(bg="lightgray")  # Reset 0 button color

        self.update_labels()

    def reset_value(self, timeline):
        """Reset the timeline value to 0 and update the total."""
        current_value = self.timeline_values[timeline]

        if current_value == 1:
            # If it was previously +1, subtract 1 from the total
            self.total -= 1
        elif current_value == -1:
            # If it was previously -1, add 1 to the total
            self.total += 1

        # Set the new value to 0
        self.timeline_values[timeline] = 0
        self.button_states[timeline]["plus"].config(bg="lightgray")  # Unhighlight +1 button
        self.button_states[timeline]["minus"].config(bg="lightgray")  # Unhighlight -1 button
        self.button_states[timeline]["zero"].config(bg="black")  # Highlight 0 button

        self.update_labels()

    def update_labels(self):
        """Update the total label."""
        self.total_label.config(text=f"Total: {self.total}")

    def update_button_colors(self, timeline):
        """Restore the button colors after loading config."""
        current_value = self.timeline_values[timeline]

        if current_value == 1:
            self.button_states[timeline]["plus"].config(bg="green")
        elif current_value == -1:
            self.button_states[timeline]["minus"].config(bg="red")
        else:
            self.button_states[timeline]["zero"].config(bg="black")

    def start_move(self, event):
        """Start moving the window."""
        self.x = event.x
        self.y = event.y

    def do_move(self, event):
        """Move the window."""
        deltax = event.x - self.x
        deltay = event.y - self.y
        self.root.geometry(f"+{self.root.winfo_x() + deltax}+{self.root.winfo_y() + deltay}")

    def disable_move(self, event):
        """Prevent window drag when clicking inside interactive widgets."""
        return

    def reset(self):
        """Reset all values to default."""
        self.timeline_values = {timeline: 0 for timeline in self.timelines}
        for timeline in self.timelines:
            self.button_states[timeline]["plus"].config(bg="lightgray")
            self.button_states[timeline]["minus"].config(bg="lightgray")
            self.button_states[timeline]["zero"].config(bg="black")
            self.fields[timeline]["entry_value"].set(0.0)
            self.fields[timeline]["stop_loss_value"].set(0.0)
            self.fields[timeline]["take_profit_value"].set(0.0)
            self.fields[timeline]["toggle_state"].set("✖")  # Reset toggle button to ✖
        self.total = 0
        self.update_labels()

    def on_close(self):
        """Save the configuration and close the application."""
        self.save_config()
        self.root.quit()

    def save_config(self):
        """Save the current timeline values and entry fields to a config file."""
        config = {
            "total": self.total,
            "timeline_values": self.timeline_values,
            "fields": {
                timeline: {
                    "entry_value": self.fields[timeline]["entry_value"].get(),
                    "stop_loss_value": self.fields[timeline]["stop_loss_value"].get(),
                    "take_profit_value": self.fields[timeline]["take_profit_value"].get(),
                    "toggle_state": self.fields[timeline]["toggle_state"].get()
                }
                for timeline in self.timelines
            }
        }
        with open("config.json", "w") as f:
            json.dump(config, f)

    def load_config(self):
        """Load the configuration from the config file if it exists."""
        if os.path.exists("config.json"):
            with open("config.json", "r") as f:
                config = json.load(f)
                self.total = config.get("total", 0)
                self.timeline_values = config.get("timeline_values", {timeline: 0 for timeline in self.timelines})
                fields_data = config.get("fields", {})
                for timeline in self.timelines:
                    self.fields[timeline]["entry_value"].set(fields_data.get(timeline, {}).get("entry_value", 0.0))
                    self.fields[timeline]["stop_loss_value"].set(fields_data.get(timeline, {}).get("stop_loss_value", 0.0))
                    self.fields[timeline]["take_profit_value"].set(fields_data.get(timeline, {}).get("take_profit_value", 0.0))
                    self.fields[timeline]["toggle_state"].set(fields_data.get(timeline, {}).get("toggle_state", "✖"))
        else:
            self.total = 0
            self.timeline_values = {timeline: 0 for timeline in self.timelines}

# Main execution point
if __name__ == "__main__":
    root = tk.Tk()
    app = TallyCounterApp(root)
    root.mainloop()
Reply
#2
It is not so straightforward.

You have to create a tkinter.ttk.Notebook(root), which then returns a Notebook-object.
Then you create an instance of tkinter.ttk.Frame(notebook_object).
Then you add the frame to a tab of the Notebook-object: notebook.add(frame).

Finally, you work with the Frames, where you can add labels, buttons etc.

A tutorial about this: https://www.geeksforgeeks.org/creating-t...n-tkinter/
You could make two classes. One for the main application and then a class for frames, which holds also the data.

Here my try:
import tkinter as tk
import json
import os
from tkinter import ttk


class TallyCounterApp:
    def __init__(self, root):
        # Initialize the total value
        self.root = root
        self.total = 0
 
        # List of timelines and their toggle values
        self.timelines = [
            "Week", "3 Day", "1 Day", "12 Hour", "8 Hour", "6 Hour", 
            "4 Hour", "3 Hour", "2 Hour", "1 Hour", "45 Min", "30 Min", 
            "15 Min", "10 Min", "5 Min"
        ]
        # Store the current value for each timeline (starts at 0)
        self.timeline_values = {timeline: 0 for timeline in self.timelines}
        self.button_states = {timeline: {"plus": None, "minus": None, "zero": None} for timeline in self.timelines}
 
        # Dictionary to hold Entry, Stop Loss, and Take Profit for each timeframe
        self.fields = {
            timeline: {
                "entry_value": tk.DoubleVar(value=0.0),
                "stop_loss_value": tk.DoubleVar(value=0.0),
                "take_profit_value": tk.DoubleVar(value=0.0),
                "toggle_state": tk.StringVar(value="✖")  # Initial toggle state for tick/cross
            }
            for timeline in self.timelines
        }
 
        # Load previous configuration if available
        self.load_config()
 
        # Bind mouse events to enable window dragging, but only on non-interactive areas
        self.root.bind("<Button-1>", self.start_move)
        self.root.bind("<B1-Motion>", self.do_move)
 
        # Create label to display total
        self.total_label = tk.Label(self.root, text=f"Total: {self.total}", font=("Arial", 24))
        self.total_label.grid(row=0, column=0, columnspan=8, pady=20)
 
        # Add column labels for Entry, Stop Loss, and Take Profit
        self.add_column_labels()
 
        # Create buttons and entry fields for each timeline
        self.create_timeline_buttons()
 
        # Create Reset button
        self.reset_button = tk.Button(self.root, text="Reset", font=("Arial", 18), command=self.reset, width=10)
        self.reset_button.grid(row=len(self.timelines) + 5, column=0, columnspan=8, pady=20)
 
        # Create close button in the top right
        self.close_button = tk.Button(self.root, text="X", font=("Arial", 18), command=self.on_close, fg="red", width=5)
        self.close_button.place(relx=1.0, rely=0.0, anchor="ne")
         
 
    def add_column_labels(self):
        """Add labels for Entry, Stop Loss, and Take Profit columns."""
        labels = ["Entry", "Stop Loss", "Take Profit", "✔/✖"]
        for i, label_text in enumerate(labels):
            label = tk.Label(self.root, text=label_text, font=("Arial", 14))
            label.grid(row=1, column=4 + i, padx=5, pady=5)
 
    def create_timeline_buttons(self):
        """Create buttons and entry fields for each timeline."""
        for i, timeline in enumerate(self.timelines):
            # Create label for the timeline
            label = tk.Label(self.root, text=timeline, font=("Arial", 14), anchor="w", width=10)
            label.grid(row=i+2, column=0, padx=10, pady=5)
 
            # Create +1, -1, and 0 buttons
            plus_button = tk.Button(self.root, text="+1", font=("Arial", 12), command=lambda t=timeline: self.modify_value(t, 1), width=5, bg="lightgray")
            plus_button.grid(row=i+2, column=1, padx=5, pady=5)
 
            minus_button = tk.Button(self.root, text="-1", font=("Arial", 12), command=lambda t=timeline: self.modify_value(t, -1), width=5, bg="lightgray")
            minus_button.grid(row=i+2, column=2, padx=5, pady=5)
 
            zero_button = tk.Button(self.root, text="0", font=("Arial", 12), command=lambda t=timeline: self.reset_value(t), width=5, bg="lightgray")
            zero_button.grid(row=i+2, column=3, padx=5, pady=5)
 
            # Store references to the buttons
            self.button_states[timeline]["plus"] = plus_button
            self.button_states[timeline]["minus"] = minus_button
            self.button_states[timeline]["zero"] = zero_button
 
            # Restore the previous button state from loaded config
            self.update_button_colors(timeline)
 
            # Create entry fields for Entry, Stop Loss, and Take Profit next to the buttons
            self.create_entry_fields(timeline, i+2)
 
            # Create tick/cross toggle button
            toggle_button = tk.Button(self.root, textvariable=self.fields[timeline]["toggle_state"], font=("Arial", 12), width=5, command=lambda t=timeline: self.toggle_state(t))
            toggle_button.grid(row=i+2, column=7, padx=5, pady=5)
 
            # Disable drag for interactive elements (buttons and entry fields)
            plus_button.bind("<Button-1>", self.disable_move)
            minus_button.bind("<Button-1>", self.disable_move)
            zero_button.bind("<Button-1>", self.disable_move)
            toggle_button.bind("<Button-1>", self.disable_move)
 
    def create_entry_fields(self, timeline, row):
        """Create entry fields for Entry, Stop Loss, and Take Profit for each timeline."""
        variables = [
            self.fields[timeline]["entry_value"],
            self.fields[timeline]["stop_loss_value"],
            self.fields[timeline]["take_profit_value"]
        ]
 
        for i, variable in enumerate(variables):
            entry = tk.Entry(self.root, textvariable=variable, font=("Arial", 12), width=8)
            entry.grid(row=row, column=4 + i, padx=5, pady=5)
 
            # Disable drag for the entry fields
            entry.bind("<Button-1>", self.disable_move)
 
    def toggle_state(self, timeline):
        """Toggle between tick (✔) and cross (✖) for the toggle button."""
        current_state = self.fields[timeline]["toggle_state"].get()
        new_state = "✔" if current_state == "✖" else "✖"
        self.fields[timeline]["toggle_state"].set(new_state)
 
    def modify_value(self, timeline, change):
        """Modify the timeline value by +1 or -1 and update the total."""
        current_value = self.timeline_values[timeline]
 
        if change == 1:  # User pressed +1
            if current_value == 0:  # From 0 to +1
                self.total += 1
            elif current_value == -1:  # From -1 to +1
                self.total += 2  # Remove -1, then add +1
            # Set the new value to +1
            self.timeline_values[timeline] = 1
            self.button_states[timeline]["minus"].config(bg="lightgray")  # Unhighlight -1 button
            self.button_states[timeline]["plus"].config(bg="green")  # Highlight +1 button
            self.button_states[timeline]["zero"].config(bg="lightgray")  # Reset 0 button color
 
        elif change == -1:  # User pressed -1
            if current_value == 0:  # From 0 to -1
                self.total -= 1
            elif current_value == 1:  # From +1 to -1
                self.total -= 2  # Remove +1, then subtract -1
            # Set the new value to -1
            self.timeline_values[timeline] = -1
            self.button_states[timeline]["plus"].config(bg="lightgray")  # Unhighlight +1 button
            self.button_states[timeline]["minus"].config(bg="red")  # Highlight -1 button
            self.button_states[timeline]["zero"].config(bg="lightgray")  # Reset 0 button color
 
        self.update_labels()
 
    def reset_value(self, timeline):
        """Reset the timeline value to 0 and update the total."""
        current_value = self.timeline_values[timeline]
 
        if current_value == 1:
            # If it was previously +1, subtract 1 from the total
            self.total -= 1
        elif current_value == -1:
            # If it was previously -1, add 1 to the total
            self.total += 1
 
        # Set the new value to 0
        self.timeline_values[timeline] = 0
        self.button_states[timeline]["plus"].config(bg="lightgray")  # Unhighlight +1 button
        self.button_states[timeline]["minus"].config(bg="lightgray")  # Unhighlight -1 button
        self.button_states[timeline]["zero"].config(bg="black")  # Highlight 0 button
 
        self.update_labels()
 
    def update_labels(self):
        """Update the total label."""
        self.total_label.config(text=f"Total: {self.total}")
 
    def update_button_colors(self, timeline):
        """Restore the button colors after loading config."""
        current_value = self.timeline_values[timeline]
 
        if current_value == 1:
            self.button_states[timeline]["plus"].config(bg="green")
        elif current_value == -1:
            self.button_states[timeline]["minus"].config(bg="red")
        else:
            self.button_states[timeline]["zero"].config(bg="black")
 
    def start_move(self, event):
        """Start moving the window."""
        self.x = event.x
        self.y = event.y
 
    def do_move(self, event):
        """Move the window."""
        deltax = event.x - self.x
        deltay = event.y - self.y
        self.root.geometry(f"+{self.root.winfo_x() + deltax}+{self.root.winfo_y() + deltay}")
 
    def disable_move(self, event):
        """Prevent window drag when clicking inside interactive widgets."""
        return
 
    def reset(self):
        """Reset all values to default."""
        self.timeline_values = {timeline: 0 for timeline in self.timelines}
        for timeline in self.timelines:
            self.button_states[timeline]["plus"].config(bg="lightgray")
            self.button_states[timeline]["minus"].config(bg="lightgray")
            self.button_states[timeline]["zero"].config(bg="black")
            self.fields[timeline]["entry_value"].set(0.0)
            self.fields[timeline]["stop_loss_value"].set(0.0)
            self.fields[timeline]["take_profit_value"].set(0.0)
            self.fields[timeline]["toggle_state"].set("✖")  # Reset toggle button to ✖
        self.total = 0
        self.update_labels()
 
    def on_close(self):
        """Save the configuration and close the application."""
        self.save_config()
        self.root.quit()
 
    def save_config(self):
        """Save the current timeline values and entry fields to a config file."""
        config = {
            "total": self.total,
            "timeline_values": self.timeline_values,
            "fields": {
                timeline: {
                    "entry_value": self.fields[timeline]["entry_value"].get(),
                    "stop_loss_value": self.fields[timeline]["stop_loss_value"].get(),
                    "take_profit_value": self.fields[timeline]["take_profit_value"].get(),
                    "toggle_state": self.fields[timeline]["toggle_state"].get()
                }
                for timeline in self.timelines
            }
        }
        with open("config.json", "w") as f:
            json.dump(config, f)
 
    def load_config(self):
        """Load the configuration from the config file if it exists."""
        if os.path.exists("config.json"):
            with open("config.json", "r") as f:
                config = json.load(f)
                self.total = config.get("total", 0)
                self.timeline_values = config.get("timeline_values", {timeline: 0 for timeline in self.timelines})
                fields_data = config.get("fields", {})
                for timeline in self.timelines:
                    self.fields[timeline]["entry_value"].set(fields_data.get(timeline, {}).get("entry_value", 0.0))
                    self.fields[timeline]["stop_loss_value"].set(fields_data.get(timeline, {}).get("stop_loss_value", 0.0))
                    self.fields[timeline]["take_profit_value"].set(fields_data.get(timeline, {}).get("take_profit_value", 0.0))
                    self.fields[timeline]["toggle_state"].set(fields_data.get(timeline, {}).get("toggle_state", "✖"))
        else:
            self.total = 0
            self.timeline_values = {timeline: 0 for timeline in self.timelines}


def setup_root() -> tk.Tk:
    root = tk.Tk()
    root.title("Tally Counter App")
    root.overrideredirect(True)  # Remove the window's title bar
    root.attributes("-topmost", True)
    return root

# Main execution point
if __name__ == "__main__":
    root = setup_root()

    notebook = ttk.Notebook(root)
    frames = []
    apps = []

    for x in range(1, 11):
        frame = ttk.Frame(notebook)
        apps.append(TallyCounterApp(frame))
        frame.pack()
        notebook.add(frame, text=f"Tab {x}")
        
    notebook.pack()
    root.mainloop()
You should think about load_config and save_config. Actually they are all the same for each tab.
Almost dead, but too lazy to die: https://sourceserver.info
All humans together. We don't need politicians!
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020