Search code examples
pythonpyqtpyqt5timeline

Create a timeline with PyQt5


I am starting a project with Python and PyQt5. I would like to have a timeline widget like that

So in the futur I would like each rectangle to be a button, but my main issue at the moment is to make the timeline stick to the width of my window.

At the moment, my timeline inherits from QFrame, and each rectangle is a QFrame too. So is it a good way to do it ? Is there a better way to do it ?

Here is what I tried :

Main.py

# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout
from PyQt5.QtGui import QColor
from Timeline import *
from Job import *

class FreelanceAssistant(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.resize(1080, 720)
        self.move(300, 300)
        self.setWindowTitle("Freelance Assistant")
        p = self.palette()
        p.setColor(self.backgroundRole(), QColor(32, 47, 60))
        self.setPalette(p)

        timeline = Timeline(self)

        timeline.addJob(Job(QDate(2017, 11, 21), QDate(2018, 1, 12), "Red Knuckles", 200, QColor(140, 67, 67)))
        timeline.addJob(Job(QDate(2018, 1, 15), QDate(2018, 1, 26), "ETC", 200, QColor(67, 76, 140)))

        window_layout= QVBoxLayout()
        print(self.getContentsMargins())
        self.setLayout(window_layout)
        window_layout.addWidget(timeline)

        self.show()

def main():
    app = QApplication(sys.argv)
    window = FreelanceAssistant()

    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Timeline.py

# -*- coding: utf-8 -*-

import sys
from PyQt5.QtCore import QDate
from PyQt5.QtWidgets import QFrame, QLabel
from PyQt5.QtGui import QColor

class Timeline(QFrame):
    def __init__(self, parent):
        super().__init__(parent)

        self.jobs_number = 0

        self.setGeometry(0, 0, parent.width(), 200)
        self.setStyleSheet("QWidget { background-color: %s }" %  QColor(26, 36, 45).name())

        self.__invoice_pos = self.height() * 0.5
        self.__invoice_height = 13
        self.__job_top_pos = self.height() * 0.5 + self.__invoice_height
        self.__job_height = (self.height() - self.__job_top_pos)/2
        self.__job_bottom_pos = self.__job_top_pos + self.__job_height

        line1 = QFrame(parent)
        line1.setGeometry(0, self.height() * 0.5, self.width(), 1)
        line1.setStyleSheet("QWidget { background-color: %s }" %  QColor(66, 76, 85).name())

        line2 = QFrame(parent)
        line2.setGeometry(0, self.height(), self.width(), 1)
        line2.setStyleSheet("QWidget { background-color: %s }" %  QColor(66, 76, 85).name())

        self.__starts_on = QDate.currentDate().addMonths(-1)
        self.__ends_on = QDate.currentDate().addMonths(5)
        self.__days_range = self.__starts_on.daysTo(self.__ends_on)

        current_year = QLabel(str(self.__starts_on.year()), parent)
        current_year.move(5, self.height() + 5)
        current_year.setStyleSheet("QWidget { color: %s }" %  QColor(245, 245, 245).name())

        for year in range(self.__starts_on.year() + 1, self.__ends_on.year() + 1):
            days_to_new_year = self.__starts_on.daysTo(QDate(year, 1, 1))
            new_year_line_pos = days_to_new_year/self.__days_range * self.width()

            new_year_line = QFrame(parent)
            new_year_line.setGeometry(int(new_year_line_pos), 0, 1, self.height() + 20)
            new_year_line.setStyleSheet("QWidget { background-color: %s }" %  QColor(245, 245, 245).name())

            current_year = QLabel(str(year), parent)
            current_year.move(int(new_year_line_pos) + 5, self.height() + 5)
            current_year.setStyleSheet("QWidget { color: %s }" %  QColor(245, 245, 245).name())

        invoice_start = QDate(2017, 11, 21)
        invoice_end = QDate(2017, 12, 13)

        invoice_start_pos = self.__starts_on.daysTo(invoice_start)/self.__days_range * self.width()
        invoice_width = invoice_start.daysTo(invoice_end)/self.__days_range * self.width()

        invoice_frame = QFrame(self)
        invoice_frame.setGeometry(invoice_start_pos, self.__invoice_pos, invoice_width, self.__invoice_height)
        invoice_frame.setStyleSheet("QWidget { background-color: %s }" %  QColor(50, 200, 50).name())

        invoice_end_date = QLabel(invoice_end.toString("dd MMM"), invoice_frame)
        invoice_end_date.move(invoice_frame.width() - 40, 0)
        invoice_end_date.setStyleSheet("QWidget { color: %s }" %  QColor(0, 0, 0).name())

    def addJob(self, job):
        start_pos = self.__starts_on.daysTo(job.starts_on)/self.__days_range * self.width()
        width = job.days_range/self.__days_range * self.width()

        job_frame = QFrame(self)
        if self.jobs_number % 2 == 0:
            job_frame.setGeometry(start_pos, self.__job_bottom_pos, width, self.__job_height)
        else:
            job_frame.setGeometry(start_pos, self.__job_top_pos, width, self.__job_height)
        job_frame.setStyleSheet("QWidget { background-color:" + job.color.name() + " } QWidget:hover{background-color: " + job.color.darker(125).name() + "}")

        company_name = QLabel(job.company, job_frame)
        company_name.move(5, 5)
        company_name.setStyleSheet("QWidget { color: %s }" %  QColor(245, 245, 245).name())

        start_date = QLabel(job.starts_on.toString("dd MMM"), job_frame)
        start_date.move(5, job_frame.height() - 20)
        start_date.setStyleSheet("QWidget { color: %s }" %  QColor(245, 245, 245).name())

        end_date = QLabel(job.ends_on.toString("dd MMM"), job_frame)
        end_date.move(job_frame.width() - 40, job_frame.height() - 20)
        end_date.setStyleSheet("QWidget { color: %s }" %  QColor(245, 245, 245).name())

        self.jobs_number += 1

Job.py

# -*- coding: utf-8 -*-

import sys
from PyQt5.QtCore import QDate
from PyQt5.QtGui import QColor

class Job():
    def __init__(self, start, end, company, rate, color):
        self.starts_on = start
        self.ends_on = end
        self.company = company
        self.rate = rate
        self.color = color
        self.days_range = self.starts_on.daysTo(self.ends_on)

Solution

  • A simple solution for this case is to overwrite the resizeEvent() method, that method is called every time the widget resizes, so we take advantage of that method to change the width of line1 and line2, but so that these QLabel can be accessed from that method must be members of the class, for this you must change line1 and line2 to self.line1 and self.line2:

    class Timeline(QFrame):
        def __init__(self, parent):
            super().__init__(parent)
    
            [...]
    
            self.line1 = QFrame(self)
            self.line1.setGeometry(0, self.height() * 0.5, self.width(), 1)
            self.line1.setStyleSheet("QWidget { background-color: %s }" %  QColor(66, 76, 85).name())
    
            self.line2 = QFrame(self)
            self.line2.setGeometry(0, self.height(), self.width(), 1)
            self.line2.setStyleSheet("QWidget { background-color: %s }" %  QColor(66, 76, 85).name())
    
            [...]
    
        def resizeEvent(self, event):
            for line in [self.line1, self.line2]:
                line.resize(self.width(), line.size().height())
            QFrame.resizeEvent(self, event)