Saturday, March 25, 2023

A simple Python Code for Un/Commenting multiple lines with PyQt

For everyone's benefits I am sharing my code snippet. As part of my on going personal Python IDE project, I need to find a way to somehow automate certain repetetive tasks that a python developer encounters when using an ordinary text editor like me. I encounter this scenario a lot wherein I have to comment out/uncomment several lines of codes. Or as a bonus, indenting/unindenting several lines of codes. I have prepared a simple python program to address this issue by using the following python program. I will update my actual project using this code :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
from PyQt5.QtWidgets import QTextEdit, QPushButton, QVBoxLayout, QApplication, QWidget, QShortcut, QMenu, QAction
from PyQt5.QtGui import QTextCursor, QKeySequence
from PyQt5.QtCore import Qt

app = QApplication([])

# Create a QTextEdit widget
text_edit = QTextEdit()

# Set the text of the widget
text_edit.setPlainText("This is a test text")

# Create a context menu for the text edit
context_menu = QMenu(text_edit)
add_hash_action = QAction("Add '#' to selected lines", context_menu)
remove_hash_action = QAction("Remove '#' from selected lines", context_menu)
add_spaces_action = QAction("Add 4 leading spaces to selected lines", context_menu)
remove_spaces_action = QAction("Remove 4 leading spaces from selected lines", context_menu)

# Define the function to be called when F3 is pressed
def insert_spaces():
    # Get the current cursor position
    cursor_position = text_edit.textCursor().position()
    cursor = text_edit.textCursor()
    selected_text = cursor.selectedText()
    # Get the text before the cursor position
    text_before_cursor = text_edit.toPlainText()[:cursor_position]
    
    # Split the text into lines
    lines = selected_text.split("\u2029")
    print(lines)
    # Add 4 spaces at the beginning of each line

    lines = ["    " + line if not line.startswith("#") else line for line in lines]
    # Join the lines and set the new text
    cursor.insertText("\n".join(lines))


# Define the function to be called when Shift+F3 is pressed
def remove_spaces():
    # Get the current cursor position and selected text
    cursor = text_edit.textCursor()
    selected_text = cursor.selectedText()

    # Split the selected text into lines
    lines = selected_text.split("\u2029")

    # Remove a 4 SPACES from the beginning of each line
    lines = [line[4:] if line.startswith("    ") else line for line in lines]

    # Replace the selected text with the modified text
    cursor.insertText("\n".join(lines))

# Define the function to add hash at the beginning of each selected line
def add_hash():
    # Get the current cursor position and selected text
    cursor = text_edit.textCursor()
    selected_text = cursor.selectedText()

    # Split the selected text into lines
    lines = selected_text.split("\u2029")
    print(lines)
    # Add a hash at the beginning of each line
    lines = ["#" + line if not line.startswith("#") else line for line in lines]

    # Replace the selected text with the modified text
    cursor.insertText("\n".join(lines))

# Define the function to remove hash from the beginning of each selected line
def remove_hash():
    # Get the current cursor position and selected text
    cursor = text_edit.textCursor()
    selected_text = cursor.selectedText()

    # Split the selected text into lines
    lines = selected_text.split("\u2029")

    # Remove a hash from the beginning of each line
    lines = [line[1:] if line.startswith("#") else line for line in lines]

    # Replace the selected text with the modified text
    cursor.insertText("\n".join(lines))
# Create a shortcut for the F3 key
shortcut_insert = QShortcut(QKeySequence("F3"), text_edit)
shortcut_insert.activated.connect(insert_spaces)

# Create a shortcut for the Shift+F3 key
shortcut_remove = QShortcut(QKeySequence("Shift+F3"), text_edit)
shortcut_remove.activated.connect(remove_spaces)

# Connect the add_spaces_action to the insert_spaces function
add_spaces_action.triggered.connect(insert_spaces)

# Connect the remove_spaces_action to the remove_spaces function
remove_spaces_action.triggered.connect(remove_spaces)

# Add the add_spaces_action and remove_spaces_action to the context menu
context_menu.addAction(add_spaces_action)
context_menu.addAction(remove_spaces_action)

# Set the context menu policy of the text edit
text_edit.setContextMenuPolicy(Qt.CustomContextMenu)
text_edit.customContextMenuRequested.connect(lambda event: context_menu.exec_(text_edit.mapToGlobal(event)))

# Create a shortcut for the F2 key to add hash
shortcut_add_hash = QShortcut(QKeySequence("F2"), text_edit)
shortcut_add_hash.activated.connect(add_hash)

# Create a shortcut for the Shift+F2 key to remove hash
shortcut_remove_hash = QShortcut(QKeySequence("Shift+F2"), text_edit)
shortcut_remove_hash.activated.connect(remove_hash)

# Connect the add_hash_action to the add_hash function
add_hash_action.triggered.connect(add_hash)

# Connect the remove_hash_action to the remove_hash function
remove_hash_action.triggered.connect(remove_hash)

# Add the add_hash_action and remove_hash_action to the context menu
context_menu.addAction(add_hash_action)
context_menu.addAction(remove_hash_action)

# Set the context menu policy of the text edit
text_edit.setContextMenuPolicy(Qt.CustomContextMenu)
text_edit.customContextMenuRequested.connect(lambda event: context_menu.exec_(text_edit.mapToGlobal(event)))

# Create a QPushButton widget
button = QPushButton("Print Selected Text")

# Define the function to be called when the button is clicked
def print_selected_text():
    selected_text = text_edit.textCursor().selectedText()
    print(f"Selected text: {selected_text}")

# Connect the button's clicked signal to the print_selected_text function
button.clicked.connect(print_selected_text)

# Create a QVBoxLayout to organize the widgets vertically
layout = QVBoxLayout()

# Add the widgets to the layout
layout.addWidget(text_edit)
layout.addWidget(button)

# Create a QWidget to hold the layout
widget = QWidget()

# Set the layout of the QWidget
widget.setLayout(layout)

# Show the QWidget
widget.show()

app.exec_()

I am also thinking of adding a feature wherein if I double-click a method, the cursor will go to the definition of that method and invent a way to make the cursor go back to its original position.

Wednesday, March 15, 2023

My Chat Application Project : 3rd Upgrade

 I uploaded the latest upgrade of my Chat Application Project to my github repository. Here is the link. To furhter check my struggles and bumps during the development you may check my logs. The latest upgrade is not so huge and the project is still a work in progress because I work 1 to 2 hours per day sometime I would work on it whenever I am in a mood. 

Here is the screenshot of the new IDE:



Here some of the upgrades:

    Chat App:

    • User can now register
    • User can now login with a password
    • Messages are now being saved in local database(SQlite3) and in the server(MySql)
    • Previous messages are now getting displayed at the chat window
    • The friend's name turns into red when that friend sends a message
    • The screen for adding a friend has been implemented but not yet working
    • The synhronization of messages(DELIVERED) between chat window and server has been implemented

    IDE:

    • Syntax Highlighting has been implemented
    • Auto Indentaion has been implemented
    • A new 'Read' pane has been added to help in proofreading a text file
    • Cursor tracking is now working(shows the row and column in realtime)
    What I am up to:
    • Currently working on the debugging window
    • Working hard to enable newly registered users add friends
   I am also planning to add the following features:
    • Planning to add comment shortcut by selecting the lines to be commented and then right click on it a context menu will appear then choose comment out selected text. It should insert the pound sign at the beginning of each line. It is also possible to insert pre formatted commenting functions such as adding comment like date and time and the project number with version number and username, etc. This will involve adding a user management system in order to identify the user editing the program and a link to the specification documents in order to identify the project number, the version, etc.
    • Planning to add a search feature wherein if it found the word, it will go to it and highlight it and there should be a status that will tell it is one out of several words it found and by pressing F7 key, it will go to the next word. 

You may test the chat app by first make sure that mysql on WAMP server is already active and by running the server first by entering python server.py and enter any letter at the textbox then click the 'Connect' button beside it. On another CMD Terminal, enter the following python popchat.py then use the following login credentials:

User: john   Password: F@c3B00p.0123

User: josh   Password: F@c3B00p.0123 

Tuesday, March 14, 2023

Detecting External Scripts in a Python Program

 Python is a popular programming language due to its simplicity, ease of use, and versatility. One of the useful tools in Python for debugging is the Python Debugger (PDB), which allows developers to interactively debug their Python code. PDB enables developers to set breakpoints, inspect variables, and step through code to help identify and resolve issues.

However, when debugging a program that imports external Python scripts, it can be challenging to detect these external scripts in PDB. The lack of visibility into external scripts can make debugging more complicated and time-consuming. In this article, we will discuss the importance of detecting external scripts in a Python program when designing a debugging window using PDB.

Figure 1: Using yesterday's post (A Better MVC Example), the sample program 
was able to detect the external python scripts



The Challenge of Debugging External Scripts
Python programs are often composed of multiple files, with each file containing classes, functions, and variables that are used across the program. These files may be located in different directories, which can make it challenging to keep track of their dependencies. External scripts are often imported into a Python program using the import statement or from statement. These external scripts may contain critical functions, classes, and variables that are essential to the program's operation.

When using PDB, the challenge arises when trying to debug a program that imports external scripts. By default, PDB only displays the current frame and does not provide visibility into external scripts. This makes it difficult to see the code in external scripts, set breakpoints, and interact with variables. This limitation can make debugging more challenging, especially if the issue is in an external script.

The Importance of Detecting External Scripts
Detecting external scripts in a Python program when designing a debugging window using PDB is crucial for efficient debugging. By detecting external scripts, developers can ensure that they have visibility into all parts of the program and can effectively debug issues.

One of the ways to detect external scripts in a Python program is to use the Abstract Syntax Tree (AST) module in Python. The AST module is part of the Python standard library and provides a way to parse Python code into an abstract syntax tree. By analyzing the AST of a Python program, developers can identify all the external scripts that the program imports. Once the external scripts are detected, they can be displayed in the debugging window, providing developers with visibility into all parts of the program.

Conclusion
In conclusion, detecting external scripts in a Python program when designing a debugging window using PDB is essential for effective debugging. By detecting external scripts, developers can ensure that they have visibility into all parts of the program and can efficiently debug issues. Python's AST module provides a way to parse Python code into an abstract syntax tree, which can be used to detect external scripts. By displaying external scripts in the debugging window, developers can have visibility into all parts of the program and effectively debug any issues.

I have prepared a simple program for this post and here is the code:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import ast
import os
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication, QFileDialog, QHBoxLayout, QLabel, QLineEdit, \
    QMainWindow, QPushButton, QListWidget, QVBoxLayout, QWidget, QListWidgetItem


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Set up the main window layout
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)

        # Add a label and QLineEdit to enter the path of the Python script
        input_layout = QHBoxLayout()
        input_label = QLabel('Python script path:')
        input_label.setFixedWidth(120)
        input_layout.addWidget(input_label)
        self.input_edit = QLineEdit()
        input_layout.addWidget(self.input_edit)
        main_layout.addLayout(input_layout)

        # Add a button to open a file dialog to select the Python script
        self.select_button = QPushButton('Select script')
        self.select_button.clicked.connect(self.select_script)
        main_layout.addWidget(self.select_button)

        # Add a label and QListWidget to display the detected Python scripts
        output_label = QLabel('Detected scripts:')
        main_layout.addWidget(output_label)
        self.output_list = QListWidget()
        main_layout.addWidget(self.output_list)

        # Set up the font for the output list
        font = QFont('Courier New', 10)
        self.output_list.setFont(font)

    def select_script(self):
        # Open a file dialog to select the Python script
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        filepath, _ = QFileDialog.getOpenFileName(self, 'Select Python script', '', 'Python Files (*.py)', options=options)

        # If a filepath was selected, analyze the script and display the detected scripts
        if filepath:
            self.input_edit.setText(filepath)
            detected_scripts = self.analyze_script(filepath)
            self.display_scripts(detected_scripts)

    def analyze_script(self, filepath):
        # Parse the Python script with AST and extract the imported scripts
        with open(filepath, 'r') as f:
            source = f.read()

        tree = ast.parse(source)
        imported_scripts = set()

        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    if not alias.name.startswith('_'):
                        imported_scripts.add(alias.name)
            elif isinstance(node, ast.ImportFrom):
                if not node.module.startswith('_'):
                    imported_scripts.add(node.module)

        # Find the full path of the imported scripts
        script_dir = os.path.dirname(filepath)
        detected_scripts = set()

        for script in imported_scripts:
            script_path = os.path.join(script_dir, script.replace('.', '/') + '.py')
            if os.path.exists(script_path):
                detected_scripts.add(script_path)

        return detected_scripts

    def display_scripts(self, scripts):
        # Clear the output list and add the detected scripts
        self.output_list.clear()
        for script in scripts:
            item = QListWidgetItem(script)
            item.setTextAlignment(Qt.AlignCenter)
            self.output_list.addItem(item)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.setWindowTitle('Python Script Analyzer')
    window.show()
    sys.exit(app.exec_())

Sunday, March 12, 2023

A Better MVC Example

Remember the last time I had posted about MVC(Python PyQt program and MySQL as a backend with a Model View Controller (MVC) design pattern)? That example was just really extremely simple and what I am thinking is what if I am working on a very large project? I want my program to be readable, extremely simplified and well organized.

My way of achieving this is to separate the the main program, model, view and controller modules into separate  files. Having it this way will reduce the size of the files I am going to edit and when it is small, it will be easier to navigate though the file and instantly hence, my productivity will increase. In post, I have created a small program that read the names of people stored in a sqlite3 database and displays the records on a QList Widget, when a name was clicked the contact details appreas on the QTextEdit widget on its righ right side.



Here is an example of the MVC I am talking about:

Introduction to the MVC Design Pattern

The Model-View-Controller (MVC) design pattern is a way of organizing code in a way that separates the user interface (View) from the underlying data (Model) and the code that manages the interaction between the two (Controller). This separation allows for easier maintenance and testing of code, and can lead to more flexible and modular applications.

The basic idea behind MVC is that the user interacts with the View, which then sends messages to the Controller, which in turn updates the Model. The Model can also send messages to the View to update the display. This way, the View and the Model are completely decoupled from each other, and only communicate through the Controller.

Construction of the Example Program

The example program provided is a simple application that displays a list of people in a database, and allows the user to select a person from the list to view their details.

The program is divided into three main components: the Model, the View, and the Controller.


Model

The Model component of the program is responsible for interacting with the database to retrieve and store data. In this program, the Model is implemented in the model.py file.


The Database class is defined in model.py. It creates a connection to a SQLite database and initializes a cursor to execute SQL commands. It also creates a table called people if it doesn't already exist, with columns for id, name, address, email, and phone.


The get_people method retrieves a list of names from the people table, and the get_person_details method retrieves the details for a specific person by name.


View

The View component of the program is responsible for displaying the user interface and interacting with the user. In this program, the View is implemented in the view.py file.


The View class is defined in view.py. It creates a QListWidget to display the list of names, and a QTextEdit to display the details of the selected person. These widgets are added to a QHBoxLayout, which is set as the layout for the View.


The set_names method takes a list of names and adds them to the QListWidget. The set_person_details method takes a tuple of details for a person and formats them into a string, which is then displayed in the QTextEdit.


Controller

The Controller component of the program is responsible for managing the interaction between the Model and the View. In this program, the Controller is implemented in the controller.py file.

The Controller class is defined in controller.py. It takes a Model and a View as arguments, and sets up the signals and slots for the interaction between the two.

The show_details method is called when the user selects a name from the QListWidget. It retrieves the details for the selected person from the Model, and then calls the set_person_details method on the View to update the display.

The initialize method is called when the Controller is created. It retrieves the list of names from the Model and calls the set_names method on the View to populate the QListWidget with the names.


Conclusion

In conclusion, the Model-View-Controller (MVC) design pattern is a powerful way to organize code in a way that separates concerns and makes code more modular and maintainable. The example program provided demonstrates how the MVC pattern can be used to create a simple application that displays data from a database. By separating the Model, View, and Controller components of the program, it becomes easier to test, maintain, and modify each part of the code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# Main
import sys
from model import Database 
from PyQt6.QtWidgets import QApplication
from view import View
from controller import Controller


if __name__ == "__main__":
    app = QApplication(sys.argv)

    # initialize model, view, and controller
    model = Database("mydatabase.db")
    view = View()
    controller = Controller(model, view)

    view.show()
    sys.exit(app.exec())

#model
import sqlite3

class Database:
    def __init__(self, db_name):
        self.connection = sqlite3.connect(db_name)
        self.cursor = self.connection.cursor()

        # create table if it doesn't exist
        self.cursor.execute("""CREATE TABLE IF NOT EXISTS people
                            (id INTEGER PRIMARY KEY,
                            name TEXT,
                            address TEXT,
                            email TEXT,
                            phone TEXT)""")
        self.connection.commit()

    def get_people(self):
        self.cursor.execute("SELECT name FROM people")
        return [row[0] for row in self.cursor.fetchall()]

    def get_person_details(self, name):
        self.cursor.execute("SELECT * FROM people WHERE name=?", (name,))
        return self.cursor.fetchone()

    def close(self):
        self.connection.close()

#controller
class Controller:
    def __init__(self, model, view):
        self.model = model
        self.view = view

        # connect signals and slots
        self.view.list_widget.itemClicked.connect(self.show_details)

        # initialize view with names from model
        self.view.set_names(self.model.get_people())

    def show_details(self, item):
        name = item.text()
        details = self.model.get_person_details(name)
        self.view.set_person_details(details)

#view
from PyQt6.QtWidgets import QListWidget, QTextEdit, QWidget, QHBoxLayout

class View(QWidget):
    def __init__(self):
        super().__init__()

        self.list_widget = QListWidget()
        self.list_widget.setFixedWidth(150)
        self.text_edit = QTextEdit()

        layout = QHBoxLayout()
        layout.addWidget(self.list_widget)
        layout.addWidget(self.text_edit)

        self.setLayout(layout)

    def set_names(self, names):
        self.list_widget.clear()
        self.list_widget.addItems(names)

    def set_person_details(self, details):
        if details:
            self.text_edit.setPlainText(f"Name: {details[1]}\n"
                                         f"Address: {details[2]}\n"
                                         f"Email: {details[3]}\n"
                                         f"Phone: {details[4]}")
        else:
            self.text_edit.clear()


Saturday, March 11, 2023

How to Save and Restore Cursor Position in Your Word Processor using Python and PyQt5

Have you ever experienced having to scroll down to your previous cursor position every time you open a file in your word processor? It's a common problem that can be both time-consuming and frustrating, especially if you're working on a lengthy document.

This can be a hassle, I did experience this while developing the IDE and working on several files. It really can complicate things if you close your file to edit other files. This is a big help and improves productivity.



Fortunately, there is a solution to this problem. By saving and restoring the cursor position in your word processor, you can ensure that every time you open a file, your cursor will automatically be placed where you left off, allowing you to pick up right where you left off and improve your productivity.

In this article, we'll take a look at a simple Python program that demonstrates how to save and restore the cursor position in a text editor built with the PyQt5 library.

Saving Cursor Position in PyQt5

PyQt5 is a set of Python bindings for the Qt application framework and runs on all platforms supported by Qt, including Windows, OS X, Linux, iOS, and Android. It allows you to create desktop applications that can run on different platforms without the need for platform-specific code.


One of the useful features of PyQt5 is its ability to handle text editing and processing. In this example, we'll create a simple text editor using the PyQt5 QTextEdit widget.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from PyQt5.QtWidgets import QApplication, QTextEdit, QMainWindow, QAction
import sys

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Create the QTextEdit widget
        self.text_edit = QTextEdit(self)
        self.setCentralWidget(self.text_edit)

        # Add a "Save" action to the File menu
        save_action = QAction("Save", self)
        save_action.setShortcut("Ctrl+S")
        save_action.triggered.connect(self.save_file)

        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(save_action)

    def save_file(self):
        # Save the contents of the QTextEdit widget to a file
        file_name, _ = QFileDialog.getSaveFileName(self, "Save file", "", "Text files (*.txt)")

        if file_name:
            with open(file_name, "w") as f:
                f.write(self.text_edit.toPlainText())

    def show(self):
        super().show()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

This code creates a QTextEdit widget and adds a "Save" action to the "File" menu. When the "Save" action is triggered, it shows a file dialog that allows you to choose a file name to save the contents of the QTextEdit widget.

Restoring Cursor Position in PyQt5

Now that we've created a simple text editor that allows us to save the contents of the QTextEdit widget, let's take a look at how we can restore the cursor position when we reopen the file.

To do this, we need to store the cursor position in a file along with the file name. Then, when we open the file, we can read the cursor position from the file and set it using the setTextCursor method of the QTextEdit widget.

Here's the updated code that demonstrates how to save and restore the cursor position in PyQt5:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from PyQt5.QtWidgets import QApplication, QTextEdit, QMainWindow, QAction, QFileDialog
from PyQt5.QtGui import QTextCursor
import sys

class MyWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # Create the QTextEdit widget
        self.text_edit = QTextEdit(self)
        self.setCentralWidget(self.text

Monday, March 6, 2023

Creating a Custom Arrow Widget with PyQt5

As part of my ongoing personal project(I am currently developing a Python IDE), I need to show which line of code is about be executed in a debugging window. 

Here is how I was able to develop the program:

But first, here is the screenshot:


Pls do not expect a really flashy output as this is just a code snippet that I want to share to evryone who might need it.

The program creates a custom arrow widget that tracks the cursor position in a QTextEdit widget and updates the arrow position accordingly. The arrow is drawn using QPainter with a black color brush and pen, and consists of an arrow shaft and arrowhead. The arrow shaft is a straight line, while the arrowhead is drawn using QPolygon and consists of three points.

Step-by-Step Explanation:

Import necessary libraries:

PyQt5.QtWidgets: provides a set of UI elements for building desktop applications.

PyQt5.QtGui: provides a set of graphical elements for building desktop applications.

PyQt5.QtCore: provides a set of core elements for building desktop applications.

Set the initial arrow position:

Initialize the arrow_x and arrow_y variables to 5 and 10, respectively.

Create a custom widget:

Define a new class called Example that inherits from the QWidget class.

Initialize the Example class by calling the super() function to call the parent class constructor.

Create a QTextEdit widget and set its position, size, and cursorPositionChanged signal to connect it to the update_arrow_position() function.

Set the Example widget's position and size.

Override the textChanged function of the QTextEdit widget with the paintEvent function of the Example widget.

Update the arrow position:

Define the update_arrow_position() function to update the arrow_y position based on the cursor position in the QTextEdit widget.

Call the update() function to repaint the widget with the updated arrow position.

Draw the arrow:

Define the paintEvent() function to draw the arrow using QPainter and QPolygon.

Draw the arrow shaft using a straight line.

Draw the arrowhead using QPolygon and three points.

Run the application:

Check if the program is being run as the main program.

Create a QApplication instance.

Create an instance of the Example widget and show it.

Run the application using the exec_() function of the QApplication instance.

Here is the source code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget , QTextEdit
from PyQt5.QtGui import QPainter, QBrush, QColor, QPen, QPolygon, QTextCursor
from PyQt5.QtCore import Qt, QPoint
arrow_x = 5
arrow_y = 10
class Example(QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)
        global arrow_x, arrow_y
        # create text edit widget
        self.text_edit = QTextEdit(self)
        self.text_edit.cursorPositionChanged.connect(self.update_arrow_position)
        self.text_edit.setGeometry(35, 5, 200,200)
        self.setGeometry(55,25,250,250)
        self.text_edit.textChanged = self.paintEvent
    def update_arrow_position(self):
        global arrow_x, arrow_y
        cursor = self.text_edit.textCursor()
        arrow_y = (cursor.blockNumber() + 1)* self.fontMetrics().height()
        self.update()
        #print(arrow_y)
        
    def paintEvent(self, e):
        global arrow_x, arrow_y
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setBrush(QBrush(Qt.black))
        painter.setPen(QPen(QColor(Qt.black), 1))
        #print(arrow_y)
        arrow_height = self.fontMetrics().height() * 1        
        arrow_width = arrow_height * 2
        #arrow_x = (self.width() - arrow_width) // 2
        #arrow_y = (self.height() - arrow_height) // 2
        #arrow_x = 5
        #arrow_y = 10
        
        # draw arrow shaft
        painter.drawLine(arrow_x, arrow_y + arrow_height // 2, arrow_x + arrow_width // 2, arrow_y + arrow_height // 2)
        
        # draw arrowhead
        points = [
            QPoint(int(arrow_x + arrow_width // 2 - arrow_height // 4), arrow_y),
            QPoint(int(arrow_x + arrow_width // 2 + arrow_height // 4), arrow_y + arrow_height // 2),
            QPoint(int(arrow_x + arrow_width // 2 - arrow_height // 4), arrow_y + arrow_height)
        ]
        painter.drawPolygon(QPolygon(points))
        
        
if __name__ == '__main__':
    app = QApplication([])
    ex = Example()
    ex.show()
    app.exec_()