Dropspot.py in a nutshell: highlight and drag rather than copy and paste.
In this guide, we’ll build a desktop app called DropSpot using Python and PyQt5. This app lets users save text snippets by dragging and dropping them into a desktop window rather than copy & paste. It organizes the saved snippets, stores them in an SQLite database, and allows you to search, edit, and delete entries.
Double-click the window to open the saved drops.

Let’s set up our environment and go through the code.
Setting Up a Virtual Environment
To keep the project’s dependencies organized, we’ll create a virtual environment. Here are the steps for both Windows and Linux:
Windows
Open a command prompt and enter:
cd C:\dropspot
python -m venv myenv
Activate the environment with:
myenv\Scripts\activate
Linux
In a terminal, navigate to your project directory and run:
cd ~/dropspot
python3 -m venv myenv
Activate it with:
source myenv/bin/activate
With the environment active, install PyQt5 and psutil, our essential dependencies:
pip install pyqt5 psutil
Now, we’re ready to start building the app.
Step 1: Database Setup with createdb.py
Our DropSpot app stores data in a local SQLite database called notepad_drops.db
. We’ll use createdb.py
to set up the database structure and provide a function to add new entries.
Code for createdb.py
import sqlite3
from datetime import datetime
def create_database():
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS drops (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
source TEXT,
timestamp TEXT NOT NULL
)''')
conn.commit()
conn.close()
def add_drop(content, source=None):
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("INSERT INTO drops (content, source, timestamp) VALUES (?, ?, ?)", (content, source, timestamp))
conn.commit()
conn.close()
# Ensure the database is created
create_database()
# Example usage of add_drop
add_drop("This is a test drop", "Example Source")
Explanation
create_database()
: Creates the database and a table calleddrops
, with columns forid
,content
,source
, andtimestamp
.add_drop(content, source=None)
: Inserts new entries into the database. If no source is provided, the default is set toNone
.
This script sets up the essential structure for our app to store snippets of text along with their source and timestamp.
Step 2: Building the Main Application with app.py
The main part of DropSpot is built in app.py
. This code defines the main app window where users can drag and drop text, a view window to manage saved entries, and various supporting functions.
Code for app.py
Here’s an overview of the key functions and classes:
import sys
import re
import psutil
import ctypes
from ctypes import wintypes
from PyQt5.QtWidgets import (QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget, QLineEdit,
QHBoxLayout, QPushButton, QScrollArea, QFrame, QInputDialog, QMessageBox)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QFont, QDesktopServices
from PyQt5.Qt import QUrl
import sqlite3
from datetime import datetime
This imports necessary modules for the app, including psutil
and ctypes
to capture the active window’s process.
The DropSpot
Class
The DropSpot
class represents the main app window where users drop text.
class DropSpot(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Notepad Drop Spot")
self.setFixedSize(200, 200)
self.setAcceptDrops(True)
self.initUI()
def initUI(self):
self.label = QLabel("Drop Text Here", self)
self.label.setAlignment(Qt.AlignCenter)
self.setCentralWidget(self.label)
- Constructor and
initUI
: Sets the window title, size, and a central label displaying “Drop Text Here.”
Handling Drag-and-Drop Events
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
text = event.mimeData().text()
self.save_drop(text)
self.label.setText("Text Saved!")
dragEnterEvent
: Checks if the dropped item is text and accepts or ignores the drop.dropEvent
: Captures the dropped text and saves it usingsave_drop()
.
Capturing the Active Window
def get_active_process_name(self):
user32 = ctypes.WinDLL('user32', use_last_error=True)
hwnd = user32.GetForegroundWindow()
pid = wintypes.DWORD()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
try:
process = psutil.Process(pid.value)
return process.name()
except psutil.NoSuchProcess:
return "Unknown Process"
get_active_process_name()
: Usesctypes
andpsutil
to capture the process name of the window in focus, storing this as the “source” in the database.
Viewing Saved Drops: The DropViewer
Class
This class is responsible for displaying saved drops, providing search, edit, and delete functionalities.
Loading Drops
def load_drops(self):
self.scroll_content.deleteLater()
self.scroll_content = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_area.setWidget(self.scroll_content)
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
search_query = f"%{self.search_bar.text()}%"
cursor.execute("SELECT id, content, source, timestamp FROM drops WHERE content LIKE ? OR source LIKE ?", (search_query, search_query))
rows = cursor.fetchall()
conn.close()
for record_id, content, source, timestamp in rows:
item_layout = QHBoxLayout()
content_display = self.format_text(content, source, timestamp)
item_layout.addWidget(content_display)
edit_button = QPushButton("Edit")
edit_button.clicked.connect(lambda checked, record_id=record_id: self.edit_entry(record_id))
item_layout.addWidget(edit_button)
delete_button = QPushButton("Delete")
delete_button.clicked.connect(lambda checked, record_id=record_id: self.delete_entry(record_id))
item_layout.addWidget(delete_button)
self.scroll_layout.addLayout(item_layout)
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
self.scroll_layout.addWidget(separator)
load_drops
: Retrieves saved entries from the database and displays them with buttons for editing and deletion. It includes a separator line for readability.
For reference, here is the entire app.py:
import sys
import re
import psutil # Replacing win32gui with psutil
import ctypes
from ctypes import wintypes
from PyQt5.QtWidgets import (QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget, QLineEdit,
QHBoxLayout, QPushButton, QScrollArea, QFrame, QInputDialog, QMessageBox)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QFont, QDesktopServices
from PyQt5.Qt import QUrl
import sqlite3
from datetime import datetime
class DropSpot(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Notepad Drop Spot")
self.setFixedSize(200, 200)
self.setAcceptDrops(True)
self.initUI()
def initUI(self):
self.label = QLabel("Drop Text Here", self)
self.label.setAlignment(Qt.AlignCenter)
self.setCentralWidget(self.label)
def dragEnterEvent(self, event):
if event.mimeData().hasText():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
text = event.mimeData().text()
self.save_drop(text)
self.label.setText("Text Saved!")
def get_active_process_name(self):
"""Helper function to get the name of the currently active process."""
user32 = ctypes.WinDLL('user32', use_last_error=True)
hwnd = user32.GetForegroundWindow()
# Get the process ID associated with the active window
pid = wintypes.DWORD()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
# Get the process name using psutil
try:
process = psutil.Process(pid.value)
return process.name()
except psutil.NoSuchProcess:
return "Unknown Process"
def save_drop(self, content):
source = self.get_active_process_name() # Get active process name as source for db field
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("INSERT INTO drops (content, source, timestamp) VALUES (?, ?, ?)", (content, source, timestamp))
conn.commit()
conn.close()
def mouseDoubleClickEvent(self, event):
self.open_viewer()
def open_viewer(self):
self.viewer = DropViewer()
self.viewer.show()
class DropViewer(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Saved Drops")
self.setGeometry(100, 100, 600, 400)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.search_bar = QLineEdit(self)
self.search_bar.setPlaceholderText("Search...")
self.search_bar.textChanged.connect(self.load_drops) # Reload entries based on search input
self.layout.addWidget(self.search_bar)
self.scroll_area = QScrollArea(self)
self.scroll_area.setWidgetResizable(True)
self.scroll_content = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_area.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll_area)
self.load_drops()
def clear_layout(self, layout):
""" Helper function to clear all items from a layout. """
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
def load_drops(self):
# Clear existing scroll_content and scroll_layout
self.scroll_content.deleteLater()
# Recreate scroll_content and scroll_layout
self.scroll_content = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_content)
self.scroll_area.setWidget(self.scroll_content)
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
search_query = f"%{self.search_bar.text()}%"
cursor.execute("SELECT id, content, source, timestamp FROM drops WHERE content LIKE ? OR source LIKE ?", (search_query, search_query))
rows = cursor.fetchall()
conn.close()
for record_id, content, source, timestamp in rows:
item_layout = QHBoxLayout()
# Format content with hyperlink detection
content_display = self.format_text(content, source, timestamp)
item_layout.addWidget(content_display)
# Edit Button
edit_button = QPushButton("Edit")
edit_button.clicked.connect(lambda checked, record_id=record_id: self.edit_entry(record_id))
item_layout.addWidget(edit_button)
# Delete Button
delete_button = QPushButton("Delete")
delete_button.clicked.connect(lambda checked, record_id=record_id: self.delete_entry(record_id))
item_layout.addWidget(delete_button)
# Add item layout to scroll layout
self.scroll_layout.addLayout(item_layout)
# Separator line
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
self.scroll_layout.addWidget(separator)
def format_text(self, content, source, timestamp):
# Wrap text if over 100 characters
formatted_content = content if len(content) <= 100 else '\n'.join([content[i:i+100] for i in range(0, len(content), 100)])
label_text = f"{timestamp} - {formatted_content} ({source if source else 'No Source'})"
label = QLabel(self)
label.setWordWrap(True)
label.setOpenExternalLinks(True) # Make URLs clickable
label.setFont(QFont("Arial", 10))
# Enhanced URL pattern to capture more complex URLs
url_pattern = r'(https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}(/[a-zA-Z0-9._~:/?#[\]@!$&\'()*+,;=%-]*)?)'
# Replace matched URLs with clickable links
formatted_label_text = re.sub(url_pattern, r'<a href="\1">\1</a>', label_text)
label.setText(formatted_label_text)
return label
def edit_entry(self, record_id):
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
cursor.execute("SELECT content FROM drops WHERE id = ?", (record_id,))
old_content = cursor.fetchone()[0]
conn.close()
# Show input dialog to edit content
new_content, ok = QInputDialog.getMultiLineText(self, "Edit Entry", "Edit content:", old_content)
if ok and new_content:
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
cursor.execute("UPDATE drops SET content = ? WHERE id = ?", (new_content, record_id))
conn.commit()
conn.close()
self.load_drops() # Refresh display to show updated entry
def delete_entry(self, record_id):
confirm = QMessageBox.question(self, "Delete Entry", "Are you sure you want to delete this entry?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if confirm == QMessageBox.Yes:
conn = sqlite3.connect("notepad_drops.db")
cursor = conn.cursor()
cursor.execute("DELETE FROM drops WHERE id = ?", (record_id,))
conn.commit()
conn.close()
self.load_drops() # Refresh display
def main():
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("dropicon.png"))
drop_spot = DropSpot()
drop_spot.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Thank you for reading this article. We hope you found it helpful and informative. If you have any questions, or if you would like to suggest new Python code examples or topics for future tutorials/articles, please feel free to join and comment. Your feedback and suggestions are always welcome!
You can find the same tutorial on Medium.com.