Skip to content

Commit 52294f8

Browse files
author
Luís Gabriel Nascimento Simas
committed
Squashed commit of the following:
commit bf69c55 Author: Gabriel Simas <autorgabrielsimas@gmail.com> Date: Sun Feb 2 15:54:07 2025 -0300 Feature/challenge resolved (#3) * Challenge OK * Atualizando o README com um novo vídeo --------- Co-authored-by: Luís Gabriel Nascimento Simas <luis.gabriel_thera@prestador.globo> commit 5af4965 Author: Gabriel Simas <autorgabrielsimas@gmail.com> Date: Fri Jan 31 12:11:34 2025 -0300 First Commit - README.md (#1) Co-authored-by: Luís Gabriel Nascimento Simas <luis.gabriel_thera@prestador.globo> # Conflicts: # README.md
1 parent 5ca6ecb commit 52294f8

File tree

9 files changed

+351
-2
lines changed

9 files changed

+351
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ venv/
135135
ENV/
136136
env.bak/
137137
venv.bak/
138+
virtualenv/
138139

139140
# Spyder project settings
140141
.spyderproject

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
## Project Description
66

77
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:
8-
9-
![Alt Text](image.gif)
8+
<p align="center">
9+
<img src="result.gif" />
10+
</p>
11+
<p align="center">
12+
<img src="result_2.gif" />
13+
</p>
1014

1115
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.
1216

main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import sys
2+
from src.main_window import MainWindow
3+
from PyQt6.QtWidgets import QApplication
4+
5+
6+
def main():
7+
app = QApplication(sys.argv)
8+
window = MainWindow()
9+
window.show()
10+
sys.exit(app.exec())
11+
12+
13+
if __name__ == "__main__":
14+
main()

result.gif

10.8 MB
Loading

result_2.gif

6.38 MB
Loading

src/image_gallery_widget.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import os
2+
from PyQt6.QtCore import Qt
3+
from src.image_model import ImageModel
4+
from PyQt6.QtWidgets import (
5+
QLabel,
6+
QWidget,
7+
QPushButton,
8+
QHBoxLayout,
9+
QVBoxLayout,
10+
)
11+
from PyQt6.QtGui import QPixmap
12+
13+
14+
class ImageGalleryWidget(QWidget):
15+
"""
16+
The central widget displaying the current image,
17+
with Previous/Next buttons. Connects to an ImageModel
18+
to load images and update the display.
19+
"""
20+
21+
def __init__(self, model: ImageModel):
22+
super().__init__()
23+
self._model = model
24+
# Widgets
25+
self._image_label = QLabel("No image loaded.")
26+
self._image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
27+
28+
self._prev_button = QPushButton("Previous")
29+
self._next_button = QPushButton("Next")
30+
# Layouts
31+
button_layout = QHBoxLayout()
32+
button_layout.addWidget(self._prev_button)
33+
button_layout.addWidget(self._next_button)
34+
35+
main_layout = QVBoxLayout()
36+
main_layout.addWidget(self._image_label, stretch=1)
37+
main_layout.addLayout(button_layout)
38+
39+
self.setLayout(main_layout)
40+
41+
# Button Signals
42+
self._prev_button.clicked.connect(self.show_previous_image)
43+
self._next_button.clicked.connect(self.show_next_image)
44+
45+
def load_current_image(self):
46+
"""
47+
Loads the current image from the model, if any.
48+
"""
49+
path = self._model.get_current_image_path()
50+
if path and os.path.isfile(path):
51+
pixmap = QPixmap(path)
52+
53+
# Optionally scale to label
54+
scale_pixmap = pixmap.scaled(
55+
self._image_label.size(),
56+
Qt.AspectRatioMode.KeepAspectRatio,
57+
Qt.TransformationMode.SmoothTransformation,
58+
)
59+
self._image_label.setPixmap(scale_pixmap)
60+
else:
61+
self._image_label.setText("No image loaded.")
62+
63+
def show_previous_image(self):
64+
self._model.previous_image()
65+
self.load_current_image()
66+
67+
def show_next_image(self):
68+
self._model.next_image()
69+
self.load_current_image()
70+
71+
def resizeEvent(self, event):
72+
"""
73+
Called when the widget is resized (so we can re-scale the image).
74+
"""
75+
super().resizeEvent(event)
76+
self.load_current_image()

src/image_model.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from src.thumbnails_list_widget import ThumbnailListWidget
2+
3+
4+
class ImageModel:
5+
"""
6+
Holds the list of image paths, and the current index.
7+
Manages navigation logic for next/previous images.
8+
"""
9+
10+
def __init__(self):
11+
self._image_paths = []
12+
self._current_index = -1
13+
self._thumbnails_list = None
14+
15+
def set_images(
16+
self,
17+
image_paths: list[str],
18+
thumbnails_list: ThumbnailListWidget,
19+
):
20+
self._image_paths = image_paths
21+
self._current_index = 0 if image_paths else -1
22+
self._thumbnails_list = thumbnails_list
23+
24+
def get_current_image_path(self):
25+
if 0 <= self._current_index < len(self._image_paths):
26+
return self._image_paths[self._current_index]
27+
return None
28+
29+
def next_image(self):
30+
if 0 <= self._current_index < len(self._image_paths) - 1:
31+
self._current_index += 1
32+
if self._thumbnails_list:
33+
self._thumbnails_list.select_index(self._current_index)
34+
35+
def previous_image(self):
36+
"""
37+
Move to the previous image, if possible.
38+
"""
39+
if self._current_index > 0:
40+
self._current_index -= 1
41+
if self._thumbnails_list:
42+
self._thumbnails_list.select_index(self._current_index)
43+
44+
def jump_to_index(self, index):
45+
if 0 <= index < len(self._image_paths):
46+
self._current_index = index
47+
if self._thumbnails_list:
48+
self._thumbnails_list.select_index(self._current_index)

src/main_window.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import os
2+
from PyQt6.QtWidgets import (
3+
QMenu,
4+
QWidget,
5+
QMenuBar,
6+
QMainWindow,
7+
QHBoxLayout,
8+
QFileDialog,
9+
QMessageBox,
10+
)
11+
12+
from PyQt6.QtCore import QTimer, Qt
13+
from src.image_model import ImageModel
14+
from PyQt6.QtGui import QKeySequence, QAction
15+
from src.image_gallery_widget import ImageGalleryWidget
16+
from src.thumbnails_list_widget import ThumbnailListWidget
17+
18+
19+
class MainWindow(QMainWindow):
20+
"""
21+
Combines the ImageGalleryWidget (center),
22+
the ThumbnailsListWidget (left), and a member for
23+
opening folders and starting/stopping a slideshow
24+
"""
25+
26+
def __init__(self):
27+
super().__init__()
28+
self.setWindowTitle("Customizable Image Gallery")
29+
self.resize(1200, 800)
30+
31+
# Model
32+
self._model: ImageModel = ImageModel()
33+
34+
# Widgets
35+
self._image_gallery_widget = ImageGalleryWidget(self._model)
36+
self._thumbnails_list = ThumbnailListWidget(self.on_thumbnails_selected)
37+
self._model.set_images([], self._thumbnails_list)
38+
# Timer for Slideshow
39+
self._slideshow_timer = QTimer()
40+
self._slideshow_timer.setInterval(2000) # 2 seconds per image
41+
self._slideshow_timer.timeout.connect(self.handle_slideshow_step)
42+
self._slideshow_running = True
43+
44+
# Layout
45+
central_widget = QWidget()
46+
main_layout = QHBoxLayout()
47+
main_layout.addWidget(self._thumbnails_list, stretch=1)
48+
main_layout.addWidget(self._image_gallery_widget, stretch=3)
49+
central_widget.setLayout(main_layout)
50+
self.setCentralWidget(central_widget)
51+
52+
# Menubar
53+
menubar = self.menuBar() if self.menuBar() else QMenuBar(self)
54+
file_menu = menubar.addMenu("File")
55+
slideshow_menu = menubar.addMenu("Slideshow")
56+
57+
open_folder_action = QAction("Open Folder", self)
58+
open_folder_action.triggered.connect(self.open_folder)
59+
file_menu.addAction(open_folder_action)
60+
61+
start_slideshow_action = QAction("Start Slideshow", self)
62+
start_slideshow_action.triggered.connect(self.start_slideshow)
63+
slideshow_menu.addAction(start_slideshow_action)
64+
65+
stop_slideshow_action = QAction("Stop Slideshow", self)
66+
stop_slideshow_action.triggered.connect(self.stop_slideshow)
67+
slideshow_menu.addAction(stop_slideshow_action)
68+
69+
# Keyboard shortcut (Left/Right arrow keys)
70+
prev_action = QAction("Previous", self)
71+
prev_action.setShortcut(QKeySequence(Qt.Key.Key_Left))
72+
prev_action.triggered.connect(self.show_previous_image)
73+
self.addAction(prev_action)
74+
75+
next_action = QAction("Next", self)
76+
next_action.setShortcut(QKeySequence(Qt.Key.Key_Right))
77+
next_action.triggered.connect(self.show_next_image)
78+
self.addAction(next_action)
79+
80+
def open_folder(self):
81+
"""
82+
Opens a folder dialog and loads images into the model
83+
"""
84+
folder_path = QFileDialog.getExistingDirectory(
85+
self,
86+
"Select Folder",
87+
)
88+
if folder_path:
89+
valid_extensions = {
90+
".png",
91+
".jpg",
92+
".jpeg",
93+
".bmp",
94+
".gif",
95+
}
96+
image_paths = [
97+
os.path.join(folder_path, f)
98+
for f in os.listdir(folder_path)
99+
if os.path.splitext(f.lower())[1] in valid_extensions
100+
]
101+
image_paths.sort()
102+
103+
if not image_paths:
104+
QMessageBox.warning(self, "Warning", "No images found in this folder")
105+
return
106+
107+
self._model.set_images(image_paths, self._thumbnails_list)
108+
109+
# Update UI
110+
self._image_gallery_widget.load_current_image()
111+
self._thumbnails_list.populate(image_paths)
112+
self._thumbnails_list.select_index(self._model._current_index)
113+
114+
def start_slideshow(self):
115+
if self._model._image_paths:
116+
self._slideshow_timer.start()
117+
self._slideshow_running = True
118+
119+
def stop_slideshow(self):
120+
self._slideshow_timer.stop()
121+
self._slideshow_running = False
122+
123+
def handle_slideshow_step(self):
124+
"""
125+
Move to the next image automatically. If we reach the end, wrap around.
126+
"""
127+
128+
if not self._model._image_paths:
129+
return
130+
131+
if self._model._current_index >= len(self._model._image_paths) - 1:
132+
# Wrap to First
133+
self._model._current_index = 0
134+
else:
135+
self._model.next_image()
136+
137+
self.update_display()
138+
139+
def on_thumbnails_selected(self, index):
140+
"""
141+
Called when user selects a thumbnail in the list.
142+
"""
143+
self._model.jump_to_index(index)
144+
self.update_display()
145+
146+
def show_previous_image(self):
147+
self._model.previous_image()
148+
self.update_display()
149+
150+
def show_next_image(self):
151+
self._model.next_image()
152+
self.update_display()
153+
154+
def update_display(self):
155+
self._image_gallery_widget.load_current_image()
156+
self._thumbnails_list.select_index(self._model._current_index)

src/thumbnails_list_widget.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
from PyQt6.QtCore import QSize
3+
from PyQt6.QtWidgets import QListWidget, QListWidgetItem
4+
5+
6+
class ThumbnailListWidget(QListWidget):
7+
"""
8+
Displays a list of image filenames (or actual thumbnails) on the side.
9+
When an item is selected, it calls a callback to let the main app
10+
switch to that image
11+
"""
12+
13+
def __init__(self, on_item_selected=None):
14+
super().__init__()
15+
self._on_item_selected = on_item_selected
16+
self.setIconSize(QSize(60, 60)) # Adjust thumbnail icon size as needed
17+
18+
# Connect the selection signal
19+
self.itemSelectionChanged.connect(self.handle_selection_changed)
20+
21+
def populate(self, image_paths):
22+
"""
23+
Clears the list and re-populates with given image paths.
24+
Here, we add items with either icons or text.
25+
"""
26+
self.clear()
27+
for path in image_paths:
28+
item = QListWidgetItem(os.path.basename(path))
29+
# If you wanna to show a small thumbnail icon:
30+
# pixmap = QPixmax(path)
31+
# icon = QIcon(
32+
# pixmap.scaled(60, 60, Qt.AspectRatioMode.KeepAspectRatio)
33+
# )
34+
# item.setIcon(icon)
35+
self.addItem(item)
36+
37+
def handle_selection_changed(self):
38+
# Use currentIndex() to get the selected item
39+
selected_item = self.currentIndex()
40+
if selected_item.isValid(): # Check if the item is selected
41+
selected_index = selected_item.row() # Get the index of the selected item
42+
if self._on_item_selected:
43+
self._on_item_selected(selected_index)
44+
45+
def select_index(self, index):
46+
"""
47+
Programmatically select an index in the list.
48+
"""
49+
if 0 <= index < self.count():
50+
self.setCurrentRow(index)

0 commit comments

Comments
 (0)