Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Build Executables for Linux, macOS, and Windows

on:
push:
tags:
- '*'

jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.x

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Build Linux executable
run: |
pyinstaller --onefile --windowed main.py
mv dist/main dist/my_app_linux

- name: Upload Linux executable
uses: softprops/action-gh-release@v1
with:
files: dist/my_app_linux
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-macos:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.x

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Build macOS executable
run: |
pyinstaller --onefile --windowed main.py
mv dist/main dist/my_app_macos

- name: Upload macOS executable
uses: softprops/action-gh-release@v1
with:
files: dist/my_app_macos
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-windows:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.x

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Build Windows executable
run: |
pyinstaller --onefile --windowed main.py
mv dist/main.exe dist/my_app_windows.exe

- name: Upload Windows executable
uses: softprops/action-gh-release@v1
with:
files: dist/my_app_windows.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ venv/
ENV/
env.bak/
venv.bak/
virtualenv/

# Spyder project settings
.spyderproject
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
*Project Level 3:* ***Real-World***

*This project is designed for learners who know Python fundamentals and are learning to build real-world programs.*

## Project Description

In this project, we will build a simple image gallery viewer where users can browse through images stored in a folder. The app will allow the user to select a folder from their computer and display the images of that folder:
<p align="center">
<img src="result.gif" />
</p>
<p align="center">
<img src="result_2.gif" />
</p>

This project is useful for building a GUI application using one of the best GUI libraries such as PyQt6, and it introduces users to managing file systems, working with images, and handling GUI events.

## Learning Benefits

- Learn how to create a basic PyQt6 GUI with interactive elements.

- Implement image navigation and display logic.

- Practice handling user input (button clicks, list selections).

## Prerequisites

**Required Libraries**:PyQt6. Install the libraries with: pip install PyQt6

**Required Files**: You need to have a folder with some images.

**IDE**: Use any IDE.
Binary file added image.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import sys
from src.main_window import MainWindow
from PyQt6.QtWidgets import QApplication


def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())


if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
altgraph==0.17.4
packaging==24.2
pefile==2023.2.7
pyinstaller==6.11.1
pyinstaller-hooks-contrib==2025.1
PyQt6==6.8.0
PyQt6-Qt6==6.8.1
PyQt6_sip==13.9.1
pywin32-ctypes==0.2.3
Binary file added result.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added result_2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 76 additions & 0 deletions src/image_gallery_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os
from PyQt6.QtCore import Qt
from src.image_model import ImageModel
from PyQt6.QtWidgets import (
QLabel,
QWidget,
QPushButton,
QHBoxLayout,
QVBoxLayout,
)
from PyQt6.QtGui import QPixmap


class ImageGalleryWidget(QWidget):
"""
The central widget displaying the current image,
with Previous/Next buttons. Connects to an ImageModel
to load images and update the display.
"""

def __init__(self, model: ImageModel):
super().__init__()
self._model = model
# Widgets
self._image_label = QLabel("No image loaded.")
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

self._prev_button = QPushButton("Previous")
self._next_button = QPushButton("Next")
# Layouts
button_layout = QHBoxLayout()
button_layout.addWidget(self._prev_button)
button_layout.addWidget(self._next_button)

main_layout = QVBoxLayout()
main_layout.addWidget(self._image_label, stretch=1)
main_layout.addLayout(button_layout)

self.setLayout(main_layout)

# Button Signals
self._prev_button.clicked.connect(self.show_previous_image)
self._next_button.clicked.connect(self.show_next_image)

def load_current_image(self):
"""
Loads the current image from the model, if any.
"""
path = self._model.get_current_image_path()
if path and os.path.isfile(path):
pixmap = QPixmap(path)

# Optionally scale to label
scale_pixmap = pixmap.scaled(
self._image_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self._image_label.setPixmap(scale_pixmap)
else:
self._image_label.setText("No image loaded.")

def show_previous_image(self):
self._model.previous_image()
self.load_current_image()

def show_next_image(self):
self._model.next_image()
self.load_current_image()

def resizeEvent(self, event):
"""
Called when the widget is resized (so we can re-scale the image).
"""
super().resizeEvent(event)
self.load_current_image()
48 changes: 48 additions & 0 deletions src/image_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from src.thumbnails_list_widget import ThumbnailListWidget


class ImageModel:
"""
Holds the list of image paths, and the current index.
Manages navigation logic for next/previous images.
"""

def __init__(self):
self._image_paths = []
self._current_index = -1
self._thumbnails_list = None

def set_images(
self,
image_paths: list[str],
thumbnails_list: ThumbnailListWidget,
):
self._image_paths = image_paths
self._current_index = 0 if image_paths else -1
self._thumbnails_list = thumbnails_list

def get_current_image_path(self):
if 0 <= self._current_index < len(self._image_paths):
return self._image_paths[self._current_index]
return None

def next_image(self):
if 0 <= self._current_index < len(self._image_paths) - 1:
self._current_index += 1
if self._thumbnails_list:
self._thumbnails_list.select_index(self._current_index)

def previous_image(self):
"""
Move to the previous image, if possible.
"""
if self._current_index > 0:
self._current_index -= 1
if self._thumbnails_list:
self._thumbnails_list.select_index(self._current_index)

def jump_to_index(self, index):
if 0 <= index < len(self._image_paths):
self._current_index = index
if self._thumbnails_list:
self._thumbnails_list.select_index(self._current_index)
Loading