Brown Note, desktop Reminder

Relieve your creative blockages with these interactive desktop reminders.
Heads up! You've already completed this tutorial.

Brown Note is a desktop notes application written in Python, using PyQt.

Notes are implemented as decoration-less windows, which can be dragged around the desktop and edited at will. Details in the notes, and their position on the desktop, is stored in a SQLite file database, via SQLAlchemy, with note details and positions being restored on each session.

Introduction to the data model

Data model

The storage of user notes in the app is handled by a SQLite file database via SQLAlchemy, using the declarative_base interface. Each note stores its identifier (id , primary key), the text content with a maximum length of 1000 chars, and the x and y positions on the screen.

python
Base = declarative_base()

class Note(Base):
    __tablename__ = 'note'
    id = Column(Integer, primary_key=True)
    text = Column(String(1000), nullable=False)
    x = Column(Integer, nullable=False, default=0)
    y = Column(Integer, nullable=False, default=0)

The creation of database tables is handled automatically at startup, which also creates the database file `notes.db` if it does not exist. The created session is used for all subsequent database operations.

python
engine = create_engine('sqlite:///notes.db')
# Initalize the database if it is not already.
Base.metadata.create_all(engine)

# Create a session to handle updates.
Session = sessionmaker(bind=engine)
session = Session()

Creating new notes

Python automatically removes objects from memory when there are no further references to them. If we create new objects, but don't assignment to a variable outside of the scope (e.g. a function) they will be deleted automatically when leaving the scope. However, while the Python object will be cleared up, Qt/C++ expects things to hang around until explicitly deleted. This can lead to some weird side effects and should be avoided.

The solution is simple: just ensure you always have a Python reference to any PyQt object you're creating. In the case of our notes, we do this using a _ACTIVE_NOTES dictionary. We add new notes to this dictionary as they are created.

python
_ACTIVE_NOTES = {}

The MainWindow itself handles adding itself to this list, so we don't need to worry about it anywhere else. This means when we create a callback function to trigger creation of a new note, the slot to do this can be as simple as creating the window.

Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PyQt5 Edition) The hands-on guide to making apps with Python — Over 10,000 copies sold!

More info Get the book

python
def create_new_note(obj=None):
    MainWindow(obj)

Starting up

When starting up we want to recreate all our existing notes on the desktop. We can do this by querying the database for all Note objects, and then creating a new MainWindow object for each one. If there aren't any we just create a blank note.

python
existing_notes = session.query(Note).all()
if len(existing_notes) == 0:
    create_new_note()
else:
    for note in existing_notes:
        create_new_note(obj=note)

app.exec_()

The Note widget

The notes are implemented as QMainWindow objects. The main in the object name might be a bit of a misnomer, since you can actually have as many of them as you like.

The design of the windows was defined first in Qt Designer, so we import this and call self.setupUi(self) to intialize. We also need to add a couple of window hint flags to the window to get the style & behaviour we're looking for — Qt.FramelessWindowHint removes the window decorations and Qt.WindowStaysOnTopHint keeps the notes on top.

python
::python
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, obj=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.show()

To complete the setup for notes we need to either store the existing Note object (from the database) or create a new one. If we're starting with an existing note we load the settings into the current window, if we've created a new one we save it to the database.

python
::python
    # Load/save note data, store this notes db reference.
    if obj:
        self.obj = obj
        self.load()
    else:
        self.obj = Note()
        self.save()

    self.closeButton.pressed.connect(self.delete_window)
    self.moreButton.pressed.connect(create_new_note)
    self.textEdit.textChanged.connect(self.save)

    # Flags to store dragged-dropped
    self._drag_active = False

This initial save just stores the position + an empty string. On a subsequent load we would have the default empty note.

We define a method to handle loading the content of a database Note object into the window, and a second to save the current settings back to the database.

python
::python
def load(self):
    self.move(self.obj.x, self.obj.y)
    self.textEdit.setHtml(self.obj.text)
    _ACTIVE_NOTES[self.obj.id] = self

def save(self):
    self.obj.x = self.x()
    self.obj.y = self.y()
    self.obj.text = self.textEdit.toHtml()
    session.add(self.obj)
    session.commit()
    _ACTIVE_NOTES[self.obj.id] = self

Both methods store to `_ACTIVE_NOTES` even though this is redundant once the first storage has occurred. This is to ensure we have a reference to the object whether we're loading from the database or saving to it.

The last step to a working notes application is to handle mouse interactions with our note windows. The interaction requirements are very basic — click to activate and drag to reposition.

The interaction is managed via three event handlers mousePressEvent, mouseMoveEvent and mouseReleaseEvent.

The press event detects a mouse down on the note window and registers the initial position.

python
::python
def mousePressEvent(self, e):
    self.previous_pos = e.globalPos()

The move event is only active while the mouse button is pressed and reports each movement, updating the current position of the note window on the screen.

python
::python
def mouseMoveEvent(self, e):
    delta = e.globalPos() - self.previous_pos
    self.move(self.x() + delta.x(), self.y()+delta.y())
    self.previous_pos = e.globalPos()

    self._drag_active = True

Finally release takes the end position of the dragged window and writes it to the database, by calling save().

python
:::python
def mouseReleaseEvent(self, e):
    if self._drag_active:
        self.save()
        self._drag_active = False

The delete note handler shows a confirmation message, then handles the delete of the Note object from the database via db.session. The final step is to close the window and delete the reference to it from _ACTIVE_NOTES. We do this by id, allowing us to delete the PyQt object reference after the Qt object has been deleted.

python
::python
def delete_window(self):
    result = QMessageBox.question(self, "Confirm delete", "Are you sure you want to delete this note?")
    if result == QMessageBox.Yes:
        note_id = self.obj.id
        session.delete(self.obj)
        session.commit()
        self.close()
        del _ACTIVE_NOTES[note_id]

Theming Notes

We want to add a bit of colour to our notes application and make them stand out on the desktop. While we could apply the colours to each element (e.g. using stylesheets) since we want to affect all windows there is a simpler way — setting the application palette.

First we create a new palette object with `QPalette()`, which will contain the current application palette defaults. Then we can override each colour in turn that we want to alter. The entries in a palette are identified by constants on `QPalette`, [see here for a full list]().

python
app = QApplication([])
app.setApplicationName("Brown Note")
app.setStyle("Fusion")

# Custom brown palette.
palette = QPalette()
palette.setColor(QPalette.Window, QColor(188,170,164))
palette.setColor(QPalette.WindowText, QColor(121,85,72))
palette.setColor(QPalette.ButtonText, QColor(121,85,72))
palette.setColor(QPalette.Text, QColor(121,85,72))
palette.setColor(QPalette.Base, QColor(188,170,164))
palette.setColor(QPalette.AlternateBase, QColor(188,170,164))
app.setPalette(palette)
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Brown Note, desktop Reminder was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.