├── .gitignore ├── LICENSE ├── README.md ├── doc └── images │ ├── usage_01.webp │ ├── usage_02.webp │ └── usage_03.webp └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | #.idea/ 166 | 167 | ### Python Patch ### 168 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 169 | poetry.toml 170 | 171 | # ruff 172 | .ruff_cache/ 173 | 174 | # LSP config files 175 | pyrightconfig.json 176 | 177 | ### VisualStudioCode ### 178 | .vscode/* 179 | .vscode/settings.json 180 | !.vscode/tasks.json 181 | !.vscode/launch.json 182 | !.vscode/extensions.json 183 | !.vscode/*.code-snippets 184 | 185 | # Local History for Visual Studio Code 186 | .history/ 187 | 188 | # Built Visual Studio Code Extensions 189 | *.vsix 190 | 191 | ### VisualStudioCode Patch ### 192 | # Ignore all local history of files 193 | .history 194 | .ionide 195 | 196 | ### Windows ### 197 | # Windows thumbnail cache files 198 | Thumbs.db 199 | Thumbs.db:encryptable 200 | ehthumbs.db 201 | ehthumbs_vista.db 202 | 203 | # Dump file 204 | *.stackdump 205 | 206 | # Folder config file 207 | [Dd]esktop.ini 208 | 209 | # Recycle Bin used on file shares 210 | $RECYCLE.BIN/ 211 | 212 | # Windows Installer files 213 | *.cab 214 | *.msi 215 | *.msix 216 | *.msm 217 | *.msp 218 | 219 | # Windows shortcuts 220 | *.lnk 221 | 222 | # End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python 223 | 224 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 225 | 226 | test* 227 | *.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 EasyDev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tkinter Quick Layout 2 | 3 | This repository will help you create complex layouts when building Tkinter and CustomTkinter . 4 | 5 | ## Features 6 | 7 | - Easy-to-use GUI for layout creation. 8 | - Generate grid values as code, copy to clipboard 9 | 10 | ## Download 11 | 12 | Go to the [GitHub releases page](https://github.com/EasyDevv/Tkinter_Quick_Layout/releases), and download the exe file from the `Assets`. 13 | 14 | ## Usage 15 | 16 | ### Basic Control 17 | 18 | - ▲ ▼ : Row 19 | - ◀ ▶ : Column 20 | - ▲▲ ▼▼ : Row span 21 | - ◀◀ ▶▶ : Column span 22 | - ✓ : Copy to clipboard 23 | - X : Remove Frame 24 | 25 | 26 | ![usage_01](./doc/images/usage_01.webp) 27 | 28 | ### Example frame setup 29 | ![usage_02](./doc/images/usage_02.webp) 30 | 31 | ### Example button setup 32 | ![usage_03](./doc/images/usage_03.webp) 33 | -------------------------------------------------------------------------------- /doc/images/usage_01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EasyDevv/Tkinter_Quick_Layout/fd2aeae4a00d7d01a700cc67729b65c5cda0db2b/doc/images/usage_01.webp -------------------------------------------------------------------------------- /doc/images/usage_02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EasyDevv/Tkinter_Quick_Layout/fd2aeae4a00d7d01a700cc67729b65c5cda0db2b/doc/images/usage_02.webp -------------------------------------------------------------------------------- /doc/images/usage_03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EasyDevv/Tkinter_Quick_Layout/fd2aeae4a00d7d01a700cc67729b65c5cda0db2b/doc/images/usage_03.webp -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pyperclip 2 | import customtkinter as ctk 3 | import random 4 | 5 | 6 | class App(ctk.CTk): 7 | def __init__(self): 8 | super().__init__() 9 | 10 | self.title("Tkinter Quick Layout") 11 | self.geometry("800x800") 12 | 13 | max_rows = 5 14 | max_columns = 5 15 | 16 | self.create_grid(self, max_rows, max_columns) 17 | self.create_bg_buttons(max_rows, max_columns) 18 | 19 | def create_grid(self, base, max_rows, max_columns, weight=1): 20 | """ 21 | Creates a grid with the specified number of rows and columns. 22 | """ 23 | for i in range(max_rows): 24 | base.grid_rowconfigure(i, weight=weight) 25 | for j in range(max_columns): 26 | base.grid_columnconfigure(j, weight=weight) 27 | 28 | def create_bg_buttons(self, max_rows, max_columns): 29 | """ 30 | Creates a grid of buttons with the specified number of rows and columns. 31 | Each button is assigned a command that calls the create_frame method with 32 | the button's row and column indices as arguments. 33 | """ 34 | for i in range(max_rows): 35 | for j in range(max_columns): 36 | button = ctk.CTkButton( 37 | self, 38 | text="+", 39 | fg_color="gray", 40 | hover_color="lightgray", 41 | command=lambda i=i, j=j: self.create_frame(i, j), 42 | ) 43 | button.grid(row=i, column=j, padx=5, pady=5, sticky="nsew") 44 | 45 | def create_frame(self, row, column): 46 | """ 47 | Creates a frame with a random background color and a grid of buttons. 48 | """ 49 | primary_color, secondary_color, tertiary_color = generate_and_adjust_colors() 50 | 51 | frame = ctk.CTkFrame(self, fg_color=primary_color, corner_radius=0) 52 | frame.grid(row=row, column=column, sticky="nsew") 53 | self.add_buttons_to_frame(frame, secondary_color, tertiary_color) 54 | 55 | def add_buttons_to_frame(self, frame, secondary_color, tertiary_color): 56 | """ 57 | Add buttons to clicked frame. 58 | """ 59 | self.create_grid(frame, 7, 7, weight=2) 60 | BUTTON_WIDTH = 35 61 | 62 | info = frame.grid_info() 63 | row = info["row"] 64 | column = info["column"] 65 | 66 | # ================================================================ 67 | # Position 68 | # ================================================================ 69 | label = ctk.CTkLabel( 70 | frame, 71 | text=f"row={row}, column={column}\nrowspan=1, columnspan=1", 72 | text_color="black", 73 | ) 74 | btn_add_row = ctk.CTkButton( 75 | frame, 76 | width=BUTTON_WIDTH, 77 | text="▼", 78 | fg_color=secondary_color, 79 | command=lambda: self.change_grid_parameters(frame, label, "row", +1), 80 | ) 81 | btn_sub_row = ctk.CTkButton( 82 | frame, 83 | width=BUTTON_WIDTH, 84 | text="▲", 85 | fg_color=secondary_color, 86 | command=lambda: self.change_grid_parameters(frame, label, "row", -1), 87 | ) 88 | btn_add_column = ctk.CTkButton( 89 | frame, 90 | width=BUTTON_WIDTH, 91 | text="▶", 92 | fg_color=secondary_color, 93 | command=lambda: self.change_grid_parameters(frame, label, "column", +1), 94 | ) 95 | btn_sub_column = ctk.CTkButton( 96 | frame, 97 | width=BUTTON_WIDTH, 98 | text="◀", 99 | fg_color=secondary_color, 100 | command=lambda: self.change_grid_parameters(frame, label, "column", -1), 101 | ) 102 | 103 | # ================================================================ 104 | # Spans 105 | # ================================================================ 106 | btn_add_row_span = ctk.CTkButton( 107 | frame, 108 | width=BUTTON_WIDTH, 109 | text="▼▼", 110 | fg_color=tertiary_color, 111 | command=lambda: self.change_grid_parameters(frame, label, "rowspan", +1), 112 | ) 113 | btn_sub_row_span = ctk.CTkButton( 114 | frame, 115 | width=BUTTON_WIDTH, 116 | text="▲▲", 117 | fg_color=tertiary_color, 118 | command=lambda: self.change_grid_parameters(frame, label, "rowspan", -1), 119 | ) 120 | btn_add_column_span = ctk.CTkButton( 121 | frame, 122 | width=BUTTON_WIDTH, 123 | text="▶▶", 124 | fg_color=tertiary_color, 125 | command=lambda: self.change_grid_parameters(frame, label, "columnspan", +1), 126 | ) 127 | btn_sub_column_span = ctk.CTkButton( 128 | frame, 129 | width=BUTTON_WIDTH, 130 | text="◀◀", 131 | fg_color=tertiary_color, 132 | command=lambda: self.change_grid_parameters(frame, label, "columnspan", -1), 133 | ) 134 | 135 | # ================================================================ 136 | # Utilities 137 | # ================================================================ 138 | btn_copy = ctk.CTkButton( 139 | frame, 140 | width=BUTTON_WIDTH, 141 | text="✓", 142 | fg_color=secondary_color, 143 | command=lambda: self.copy_info(frame), 144 | ) 145 | btn_close = ctk.CTkButton( 146 | frame, 147 | width=BUTTON_WIDTH, 148 | text="X", 149 | fg_color=secondary_color, 150 | command=lambda: self.close_frame(frame), 151 | ) 152 | 153 | # ================================================================ 154 | # Grid 155 | # ================================================================ 156 | label.grid(row=0, column=1, columnspan=5, padx=20, pady=5, sticky="nsew") 157 | btn_sub_row.grid(row=2, column=3, padx=0, pady=0) 158 | btn_add_row.grid(row=4, column=3, padx=0, pady=0) 159 | btn_add_column.grid(row=3, column=4, padx=0, pady=0) 160 | btn_sub_column.grid(row=3, column=2, padx=0, pady=0) 161 | 162 | btn_sub_row_span.grid(row=1, column=3, padx=0, pady=0) 163 | btn_add_row_span.grid(row=5, column=3, padx=0, pady=0) 164 | btn_add_column_span.grid(row=3, column=5, padx=0, pady=0) 165 | btn_sub_column_span.grid(row=3, column=1, padx=0, pady=0) 166 | btn_copy.grid(row=3, column=3, padx=0, pady=0) 167 | btn_close.grid(row=5, column=5, padx=0, pady=0) 168 | 169 | def change_grid_parameters(self, frame, label, param="row", operation=+1): 170 | """ 171 | Changes the specified grid parameter of the frame. 172 | """ 173 | if param not in {"row", "column", "rowspan", "columnspan"}: 174 | raise ValueError(f"Invalid parameter: {param}") 175 | 176 | min_value = 1 if "span" in param else 0 177 | 178 | var = frame.grid_info()[param] + operation 179 | var = max(var, min_value) 180 | 181 | if param == "row": 182 | frame.grid(row=var) 183 | elif param == "column": 184 | frame.grid(column=var) 185 | elif param == "rowspan": 186 | frame.grid(rowspan=var) 187 | elif param == "columnspan": 188 | frame.grid(columnspan=var) 189 | 190 | info = frame.grid_info() 191 | row = info["row"] 192 | column = info["column"] 193 | row_span = info["rowspan"] 194 | column_span = info["columnspan"] 195 | 196 | label.configure( 197 | text=f"row={row}, column={column}\n rowspan={row_span}, columnspan={column_span}" 198 | ) 199 | 200 | def copy_info(self, frame): 201 | """ 202 | Copies the frame's grid information to the clipboard. 203 | """ 204 | info = frame.grid_info() 205 | 206 | pyperclip.copy( 207 | f"row={info['row']}, column={info['column']}, rowspan={info['rowspan']}, columnspan={info['columnspan']}," 208 | ) 209 | 210 | def close_frame(self, frame): 211 | frame.destroy() 212 | 213 | 214 | def generate_and_adjust_colors(factor1=0.6, factor2=0.4): 215 | """ 216 | Generates a random color and returns it along with two adjusted versions. 217 | """ 218 | r, g, b = (random.randint(0, 255) for _ in range(3)) 219 | primary_color = "#{:02x}{:02x}{:02x}".format(r, g, b) 220 | 221 | second_r, second_g, second_b = (max(0, int(color * factor1)) for color in (r, g, b)) 222 | secondary_color = "#{:02x}{:02x}{:02x}".format(second_r, second_g, second_b) 223 | 224 | third_r, third_g, third_b = (max(0, int(color * factor2)) for color in (r, g, b)) 225 | tertiary_color = "#{:02x}{:02x}{:02x}".format(third_r, third_g, third_b) 226 | 227 | return primary_color, secondary_color, tertiary_color 228 | 229 | 230 | def adjust_rgb(r, g, b, factor=0.8): 231 | """ 232 | Adjusts the specified RGB color by the specified factor. 233 | """ 234 | r = max(0, int(r * factor)) 235 | g = max(0, int(g * factor)) 236 | b = max(0, int(b * factor)) 237 | return r, g, b 238 | 239 | 240 | if __name__ == "__main__": 241 | try: 242 | print("Starting...") 243 | app = App() 244 | app.mainloop() 245 | except Exception as e: 246 | with open("error.txt", "w") as f: 247 | f.write(e) 248 | --------------------------------------------------------------------------------