1、概述
源碼放在文章末尾
根據上一篇文章回顧下利用Qt+C++實現了一個簡易的python編譯器,類似pycharm或vsCode這樣的編譯器,該python編譯器目前實現了如下功能:
(1)支持編寫python程序
(2)編寫代碼時有代碼補全提示
(3)程序運行到每行時該行高亮顯示
(4)可以加載python腳本執行
(5)可以在程序運行的過程中隨時中斷
(6)有輸出窗口實時顯示程序執行的狀態或執行程序的打印顯示等
詳細介紹可以看我上一篇文章。
在這篇文章中對上一版代碼進行了一些優化和修改,具體修改功能如下:
(1)美化了界面操作,更像一個簡易的python編譯器
(2)新增了代碼斷點調試功能
(3)新增了菜單欄,功能分別為一鍵加載python腳本、運行python腳本、停止運行、單步調試、連續調試、清空所有斷點,如下所示
下圖為Python編譯器的demo演示流程:
1、一鍵加載python腳本
關鍵代碼如下所示(注意:加載python腳本時不能有中文路徑,不然無法識別):
void pythonRecipeWidget::on_loadScriptPushButton_clicked()
{QString initialDir;QString filePath = QFileDialog::getOpenFileName(this, tr("Select Script"), initialDir);if (filePath.isEmpty())return;ui.scriptPlainTextEdit->clear();std::ifstream file(filePath.toStdString());std::string script((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());ui.scriptPlainTextEdit->setPlainText(script.c_str());file.close();
}
2、運行python腳本
給的例子中的python腳本會運行完for循環然后繼續往下運行時會正常報錯,因為沒有導入該第三方工作庫
3、停止運行
4、斷點單步調試
5、連續調試
6、清除所有斷點
主要代碼分析:
運行 Python 腳本:使用 PyRun_SimpleString。
設置斷點:通過 MyTrace 回調攔截 PyTrace_LINE。
斷點調試(斷點、暫停、繼續、單步執行)。
中斷機制(用戶手動終止腳本)。
線程安全處理(QtConcurrent::run + PyGILState_Ensure)。
UI 控制啟用/禁用(通過 QMetaObject::invokeMethod)。
QtConcurrent::run([=](){qDebug() << __FUNCTION__ << QThread::currentThreadId() << QThread::currentThread();QMetaObject::invokeMethod(m_run, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));QMetaObject::invokeMethod(m_stop, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));QMetaObject::invokeMethod(m_stepOver, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));QMetaObject::invokeMethod(m_continueExecute, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));PyGILState_STATE gstate = PyGILState_Ensure();scriptThreadState = PyThreadState_Get();PyEval_SetTrace(MyTrace, NULL);PyRun_SimpleString(GBK_To_UTF8(g_script).c_str());scriptThreadState = nullptr;PyGILState_Release(gstate);QMetaObject::invokeMethod(m_run, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));QMetaObject::invokeMethod(m_stop, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));QMetaObject::invokeMethod(m_stepOver, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));QMetaObject::invokeMethod(m_continueExecute, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, false));is_paused = false;step_once = false;});
在后臺線程中運行 Python 腳本,確保線程間 GIL 安全。
scriptThreadState 保存當前線程狀態,供中斷使用。
設置了 MyTrace 作為追蹤函數,實現在某些行/事件進行控制,MyTrace實現對代碼的斷點、停止、繼續運行等功能
int MyTrace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg)
{if (g_isExection)return 0;//如果把中斷程序放在這里會導致比如在第二行中斷時會在第二行執行完才中斷if ((lineCount == PyFrame_GetLineNumber(frame)) && what == PyTrace_EXCEPTION){g_isExection = true;int line = PyFrame_GetLineNumber(frame);return 0;}if (what == PyTrace_LINE){char const* fileName = _PyUnicode_AsString(frame->f_code->co_filename);char const* name = _PyUnicode_AsString(frame->f_code->co_name);if (strcmp(fileName, "<string>") == 0 && strcmp(name, "__new__") != 0){int line = PyFrame_GetLineNumber(frame);lineCount = line;ShowLine(line);qDebug() << "filename" << fileName << "name" << name << "line" << line << "frame" << frame << "f_back" << frame->f_back;breakPointAfter = line;//如果斷點不在代碼行上就移到下面最近的代碼行for (auto breakPointLine : breakPoints){if (breakPointBefore < breakPointLine && breakPointLine < breakPointAfter){breakPointCallBack_(breakPointBefore, breakPointAfter);is_paused = true;step_once = false;breakPointBefore = line;break;}}//當前代碼行等于斷點行就暫停程序if (breakPointAfter != breakPointBefore){for (auto breakPointLine : breakPoints){if (line == breakPointLine){is_paused = true;step_once = false;break;}}}breakPointBefore = line;//判斷當前是否debuggingif (is_paused && !step_once)bpDebuggingLineCallBack_(line, true);elsebpDebuggingLineCallBack_(line, false);// 暫停執行,等待繼續調試信號while (is_paused && !step_once) {std::this_thread::sleep_for(std::chrono::milliseconds(100));}// 單步執行一次后繼續暫停if (step_once) {is_paused = true;step_once = false;}//如果把中斷程序放在這里會導致比如在第二行中斷時會在第二行執行前中斷if (g_isAbort){if (!m_isInterrupt){qDebug() << "User abort.";//PyErr_SetString(PyExc_KeyboardInterrupt, "User abort.");if (scriptThreadState){PyGILState_STATE gstate = PyGILState_Ensure();PyThreadState_SetAsyncExc((unsigned long)scriptThreadState->thread_id, PyExc_KeyboardInterrupt);PyGILState_Release(gstate);}m_isInterrupt = true;}bpDebuggingLineCallBack_(line, false);return 0;}}}return 0;
}
追蹤函數 MyTrace
追蹤函數通過判斷 what == PyTrace_LINE 來對 Python 腳本執行的每一行做攔截,并根據斷點及狀態決定:
for (auto breakPointLine : breakPoints)
{if (line == breakPointLine){is_paused = true;step_once = false;break;}
}
命中斷點:暫停程序。
ShowLine(line) 和 bpDebuggingLineCallBack_() 用于 UI 更新。
單步執行邏輯
while (is_paused && !step_once) {std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
is_paused 和 step_once 控制主調試循環。
外部通過點擊 “繼續” 或 “單步” 按鈕控制 is_paused 和 step_once 變量。
中斷處理(abort)
if (g_isAbort && !m_isInterrupt)
{PyThreadState_SetAsyncExc((unsigned long)scriptThreadState->thread_id, PyExc_KeyboardInterrupt);m_isInterrupt = true;
}
用戶點擊“停止”按鈕時觸發中斷。
使用 PyThreadState_SetAsyncExc 強行注入 KeyboardInterrupt 異常。
斷點跳轉優化
if (breakPointBefore < breakPointLine && breakPointLine < breakPointAfter)