Brown Note | Desktop notes app using SQLAlchemy & PyQt

Relieve your creative blockages with these interactive desktop reminders.

Brown Note is a desktop notes application written in Python, using PyQt. The notes are implemented as decoration-less windows, which can be dragged around the desktop and edited. 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.

Brown Note.

Data model

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

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.

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 removing 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 your 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.

_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.

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

The note widget (a QMainWindow)

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.

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, *args, obj=None, **kwargs):
        super(MainWindow, self).__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.

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

        # 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

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.

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.

    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

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.

    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.

    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().

    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.

    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 the 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.

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)

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.

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_()

Ready to build your own apps?

Then you might enjoy this book! Create Simple GUI Applications with Python & Qt is my guide to building cross-platform GUI applications with Python. Work step by step from displaying your first window to building fully functional desktop software.

Further ideas

The text editor is handled using a QTextEdit plain text editor widget meaning there is no support for rich text. Adding support for basic formatting (bold, italic, etc.) could be added by enabling rich text mode and loading/saving via html to the database. Checks would be needed to ensure formatted text doesn't exceed the database row size and lose closing tags.

Continue reading

Failamp  gui

Failamp is a simple audio & video mediaplayer implemented in Python, using the built-in Qt playlist and media handling features. It is modelled, very loosely on the original Winamp, although nowhere near as complete (hence the fail). The main window The main window UI was built using Qt Designer. The screenshot ... More

Discussion

comments powered by Disqus