[{"data":1,"prerenderedAt":1723},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis":3},{"id":4,"title":5,"body":6,"description":1712,"extension":1713,"meta":1714,"navigation":264,"path":1719,"seo":1720,"stem":1721,"__hash__":1722},"docs\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002Findex.md","Show Progress and Support Cancellation in a QGIS Plugin",{"type":7,"value":8,"toc":1699},"minimark",[9,13,17,36,183,188,215,219,482,503,507,510,752,775,779,782,918,942,1022,1026,1029,1035,1041,1047,1118,1130,1134,1137,1378,1387,1390,1451,1455,1461,1534,1538,1598,1602,1616,1620,1626,1635,1648,1654,1664,1668,1695],[10,11,5],"h1",{"id":12},"show-progress-and-support-cancellation-in-a-qgis-plugin",[14,15,16],"p",{},"Users forgive slow. They do not forgive silent. A job that takes ninety seconds and shows a moving bar with a cancel button feels like a tool; the same ninety seconds with a frozen dialog feels like a fault. The mechanics are small — a signal, a bar, a flag checked in a loop — and the difference in how the plugin is perceived is enormous.",[14,18,19,20,25,26,30,31,35],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002F","Background Tasks and Plugin Performance",". It assumes the work is already on a ",[27,28,29],"code",{},"QgsTask"," — see ",[21,32,34],{"href":33},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Long Task with QgsTask in PyQGIS"," — and covers wiring progress to a widget, using the QGIS message bar instead of your own dialog, cancelling promptly, and leaving the interface in a sane state afterwards.",[14,37,38],{},[39,40,45,49,53,60,77,86,95,100,105,115,120,125,129,134,139,142,145,148,151,156,160,163,166,169,177,179],"svg",{"viewBox":41,"role":42,"ariaLabel":43,"xmlns":44},"0 0 760 268","img","Three places a plugin can report progress: the QGIS task manager panel, a message bar widget across the top of the canvas, and a progress bar inside the plugin's own dialog","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[46,47,48],"title",{},"Three places to show that something is happening",[50,51,52],"desc",{},"The task manager panel shows every running task automatically and costs no code. A message bar item across the top of the canvas suits a job started from a dialog the user then closes. A progress bar inside the plugin dialog suits work the user is watching directly. All three read the same progressChanged signal.",[54,55],"rect",{"x":56,"y":56,"width":57,"height":58,"fill":59},"0","760","268","#f6f3ea",[61,62,63],"defs",{},[64,65,72],"marker",{"id":66,"viewBox":67,"refX":68,"refY":69,"markerWidth":70,"markerHeight":70,"orient":71},"progArrow","0 0 10 10","8","5","7","auto-start-reverse",[73,74],"path",{"d":75,"fill":76},"M0 0 L10 5 L0 10 z","#2f3b35",[78,79,85],"text",{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","One signal, three audiences",[54,87],{"x":88,"y":89,"width":90,"height":91,"rx":68,"fill":92,"stroke":93,"style":94},"288","48","184","46","#eef7f4","#0f766e","stroke-width:2.5",[78,96,99],{"x":80,"y":97,"style":98,"fill":93,"textAnchor":84},"70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","task.progressChanged",[78,101,104],{"x":80,"y":102,"style":103,"fill":76,"textAnchor":84},"87","text-anchor:middle;font-size:10px;font-family:sans-serif","delivered on the main thread",[54,106],{"x":107,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":113,"style":114},"20","140","220","104","10","#eff3ff","#2563eb","stroke-width:2",[78,116,119],{"x":117,"y":118,"style":98,"fill":113,"textAnchor":84},"130","166","task manager panel",[78,121,124],{"x":117,"y":122,"style":123,"fill":76,"textAnchor":84},"190","text-anchor:middle;font-size:11px;font-family:sans-serif","free — no code at all",[78,126,128],{"x":117,"y":127,"style":123,"fill":76,"textAnchor":84},"212","cancel button included",[78,130,133],{"x":117,"y":131,"style":123,"fill":132,"textAnchor":84},"232","#59645f","easy to miss in the corner",[54,135],{"x":136,"y":108,"width":109,"height":110,"rx":111,"fill":137,"stroke":138,"style":94},"270","#edf8e9","#15803d",[78,140,141],{"x":80,"y":118,"style":98,"fill":138,"textAnchor":84},"message bar widget",[78,143,144],{"x":80,"y":122,"style":123,"fill":76,"textAnchor":84},"across the top of the canvas",[78,146,147],{"x":80,"y":127,"style":123,"fill":76,"textAnchor":84},"survives closing the dialog",[78,149,150],{"x":80,"y":131,"style":123,"fill":138,"textAnchor":84},"best default for plugins",[54,152],{"x":153,"y":108,"width":109,"height":110,"rx":111,"fill":154,"stroke":155,"style":114},"520","#fdf2e2","#b45309",[78,157,159],{"x":158,"y":118,"style":98,"fill":155,"textAnchor":84},"630","bar in your dialog",[78,161,162],{"x":158,"y":122,"style":123,"fill":76,"textAnchor":84},"right where they are looking",[78,164,165],{"x":158,"y":127,"style":123,"fill":76,"textAnchor":84},"must handle the dialog closing",[78,167,168],{"x":158,"y":131,"style":123,"fill":132,"textAnchor":84},"most code to get right",[170,171],"line",{"x1":172,"y1":173,"x2":174,"y2":175,"stroke":76,"style":176},"330","94","180","134","stroke-width:2;marker-end:url(#progArrow)",[170,178],{"x1":80,"y1":173,"x2":80,"y2":175,"stroke":76,"style":176},[170,180],{"x1":181,"y1":173,"x2":182,"y2":175,"stroke":76,"style":176},"430","580",[184,185,187],"h2",{"id":186},"prerequisites","Prerequisites",[189,190,191,202,209],"ul",{},[192,193,194,198,199,201],"li",{},[195,196,197],"strong",{},"QGIS 3.34 LTR"," or newer and a plugin that already runs its work on a ",[27,200,29],{},".",[192,203,204,205,201],{},"A dialog built in Qt Designer or in code — see ",[21,206,208],{"href":207},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for GIS Interfaces",[192,210,211,214],{},[27,212,213],{},"iface"," available in your plugin, which the standard plugin skeleton provides.",[184,216,218],{"id":217},"drive-a-progress-bar-from-the-task","Drive a progress bar from the task",[220,221,226],"pre",{"className":222,"code":223,"language":224,"meta":225,"style":225},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtWidgets import QProgressBar\nfrom qgis.core import QgsApplication\n\ndef start(self):\n    self.task = AreaSumTask(\"Summing parcel areas\", self.snapshot)\n\n    self.dlg.progressBar.setRange(0, 100)\n    self.dlg.progressBar.setValue(0)\n    self.dlg.cancelButton.setEnabled(True)\n\n    self.task.progressChanged.connect(self.on_progress)\n    self.task.taskCompleted.connect(self.on_done)\n    self.task.taskTerminated.connect(self.on_done)\n\n    QgsApplication.taskManager().addTask(self.task)\n\ndef on_progress(self, value):\n    self.dlg.progressBar.setValue(int(value))\n\ndef on_done(self):\n    self.dlg.progressBar.setValue(100)\n    self.dlg.cancelButton.setEnabled(False)\n","python","",[27,227,228,246,259,266,279,308,313,331,343,356,361,374,387,399,404,415,420,431,444,449,459,470],{"__ignoreMap":225},[229,230,232,236,240,243],"span",{"class":170,"line":231},1,[229,233,235],{"class":234},"snl16","from",[229,237,239],{"class":238},"s95oV"," qgis.PyQt.QtWidgets ",[229,241,242],{"class":234},"import",[229,244,245],{"class":238}," QProgressBar\n",[229,247,249,251,254,256],{"class":170,"line":248},2,[229,250,235],{"class":234},[229,252,253],{"class":238}," qgis.core ",[229,255,242],{"class":234},[229,257,258],{"class":238}," QgsApplication\n",[229,260,262],{"class":170,"line":261},3,[229,263,265],{"emptyLinePlaceholder":264},true,"\n",[229,267,269,272,276],{"class":170,"line":268},4,[229,270,271],{"class":234},"def",[229,273,275],{"class":274},"svObZ"," start",[229,277,278],{"class":238},"(self):\n",[229,280,282,286,289,292,295,299,302,305],{"class":170,"line":281},5,[229,283,285],{"class":284},"sDLfK","    self",[229,287,288],{"class":238},".task ",[229,290,291],{"class":234},"=",[229,293,294],{"class":238}," AreaSumTask(",[229,296,298],{"class":297},"sU2Wk","\"Summing parcel areas\"",[229,300,301],{"class":238},", ",[229,303,304],{"class":284},"self",[229,306,307],{"class":238},".snapshot)\n",[229,309,311],{"class":170,"line":310},6,[229,312,265],{"emptyLinePlaceholder":264},[229,314,316,318,321,323,325,328],{"class":170,"line":315},7,[229,317,285],{"class":284},[229,319,320],{"class":238},".dlg.progressBar.setRange(",[229,322,56],{"class":284},[229,324,301],{"class":238},[229,326,327],{"class":284},"100",[229,329,330],{"class":238},")\n",[229,332,334,336,339,341],{"class":170,"line":333},8,[229,335,285],{"class":284},[229,337,338],{"class":238},".dlg.progressBar.setValue(",[229,340,56],{"class":284},[229,342,330],{"class":238},[229,344,346,348,351,354],{"class":170,"line":345},9,[229,347,285],{"class":284},[229,349,350],{"class":238},".dlg.cancelButton.setEnabled(",[229,352,353],{"class":284},"True",[229,355,330],{"class":238},[229,357,359],{"class":170,"line":358},10,[229,360,265],{"emptyLinePlaceholder":264},[229,362,364,366,369,371],{"class":170,"line":363},11,[229,365,285],{"class":284},[229,367,368],{"class":238},".task.progressChanged.connect(",[229,370,304],{"class":284},[229,372,373],{"class":238},".on_progress)\n",[229,375,377,379,382,384],{"class":170,"line":376},12,[229,378,285],{"class":284},[229,380,381],{"class":238},".task.taskCompleted.connect(",[229,383,304],{"class":284},[229,385,386],{"class":238},".on_done)\n",[229,388,390,392,395,397],{"class":170,"line":389},13,[229,391,285],{"class":284},[229,393,394],{"class":238},".task.taskTerminated.connect(",[229,396,304],{"class":284},[229,398,386],{"class":238},[229,400,402],{"class":170,"line":401},14,[229,403,265],{"emptyLinePlaceholder":264},[229,405,407,410,412],{"class":170,"line":406},15,[229,408,409],{"class":238},"    QgsApplication.taskManager().addTask(",[229,411,304],{"class":284},[229,413,414],{"class":238},".task)\n",[229,416,418],{"class":170,"line":417},16,[229,419,265],{"emptyLinePlaceholder":264},[229,421,423,425,428],{"class":170,"line":422},17,[229,424,271],{"class":234},[229,426,427],{"class":274}," on_progress",[229,429,430],{"class":238},"(self, value):\n",[229,432,434,436,438,441],{"class":170,"line":433},18,[229,435,285],{"class":284},[229,437,338],{"class":238},[229,439,440],{"class":284},"int",[229,442,443],{"class":238},"(value))\n",[229,445,447],{"class":170,"line":446},19,[229,448,265],{"emptyLinePlaceholder":264},[229,450,452,454,457],{"class":170,"line":451},20,[229,453,271],{"class":234},[229,455,456],{"class":274}," on_done",[229,458,278],{"class":238},[229,460,462,464,466,468],{"class":170,"line":461},21,[229,463,285],{"class":284},[229,465,338],{"class":238},[229,467,327],{"class":284},[229,469,330],{"class":238},[229,471,473,475,477,480],{"class":170,"line":472},22,[229,474,285],{"class":284},[229,476,350],{"class":238},[229,478,479],{"class":284},"False",[229,481,330],{"class":238},[14,483,484,487,488,491,492,487,495,498,499,502],{},[195,485,486],{},"Breakdown:"," ",[27,489,490],{},"progressChanged"," carries a float between 0 and 100 and is emitted on the main thread, so updating a widget from the slot is safe. Connecting ",[195,493,494],{},"both",[27,496,497],{},"taskCompleted"," and ",[27,500,501],{},"taskTerminated"," is the detail that gets missed: the first fires only on success, the second on failure or cancellation, and a handler attached to only one leaves the dialog stuck showing a running job forever. Setting the range explicitly to 0–100 avoids the \"busy\" indeterminate style that a default-constructed bar shows.",[184,504,506],{"id":505},"prefer-the-message-bar-for-long-work","Prefer the message bar for long work",[14,508,509],{},"If the job outlives the dialog — and users close dialogs — putting the bar in the dialog is the wrong home for it. QGIS's message bar hosts widgets, including a progress bar and a cancel button.",[220,511,513],{"className":222,"code":512,"language":224,"meta":225,"style":225},"from qgis.PyQt.QtWidgets import QProgressBar\nfrom qgis.PyQt.QtCore import Qt\nfrom qgis.core import Qgis\n\ndef start_with_message_bar(self):\n    self.message_widget = iface.messageBar().createMessage(\"Summing parcel areas…\")\n\n    bar = QProgressBar()\n    bar.setRange(0, 100)\n    bar.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)\n    self.message_widget.layout().addWidget(bar)\n\n    iface.messageBar().pushWidget(self.message_widget, Qgis.Info)\n\n    self.task = AreaSumTask(\"Summing parcel areas\", self.snapshot)\n    self.task.progressChanged.connect(bar.setValue)\n    self.task.taskCompleted.connect(self.clear_message)\n    self.task.taskTerminated.connect(self.clear_message)\n    QgsApplication.taskManager().addTask(self.task)\n\ndef clear_message(self):\n    iface.messageBar().popWidget(self.message_widget)\n    iface.messageBar().pushMessage(\"Done\", \"Areas summed\", level=Qgis.Success, duration=5)\n",[27,514,515,525,537,548,552,561,578,582,592,605,616,623,627,637,641,659,666,677,687,695,699,708,718],{"__ignoreMap":225},[229,516,517,519,521,523],{"class":170,"line":231},[229,518,235],{"class":234},[229,520,239],{"class":238},[229,522,242],{"class":234},[229,524,245],{"class":238},[229,526,527,529,532,534],{"class":170,"line":248},[229,528,235],{"class":234},[229,530,531],{"class":238}," qgis.PyQt.QtCore ",[229,533,242],{"class":234},[229,535,536],{"class":238}," Qt\n",[229,538,539,541,543,545],{"class":170,"line":261},[229,540,235],{"class":234},[229,542,253],{"class":238},[229,544,242],{"class":234},[229,546,547],{"class":238}," Qgis\n",[229,549,550],{"class":170,"line":268},[229,551,265],{"emptyLinePlaceholder":264},[229,553,554,556,559],{"class":170,"line":281},[229,555,271],{"class":234},[229,557,558],{"class":274}," start_with_message_bar",[229,560,278],{"class":238},[229,562,563,565,568,570,573,576],{"class":170,"line":310},[229,564,285],{"class":284},[229,566,567],{"class":238},".message_widget ",[229,569,291],{"class":234},[229,571,572],{"class":238}," iface.messageBar().createMessage(",[229,574,575],{"class":297},"\"Summing parcel areas…\"",[229,577,330],{"class":238},[229,579,580],{"class":170,"line":315},[229,581,265],{"emptyLinePlaceholder":264},[229,583,584,587,589],{"class":170,"line":333},[229,585,586],{"class":238},"    bar ",[229,588,291],{"class":234},[229,590,591],{"class":238}," QProgressBar()\n",[229,593,594,597,599,601,603],{"class":170,"line":345},[229,595,596],{"class":238},"    bar.setRange(",[229,598,56],{"class":284},[229,600,301],{"class":238},[229,602,327],{"class":284},[229,604,330],{"class":238},[229,606,607,610,613],{"class":170,"line":358},[229,608,609],{"class":238},"    bar.setAlignment(Qt.AlignLeft ",[229,611,612],{"class":234},"|",[229,614,615],{"class":238}," Qt.AlignVCenter)\n",[229,617,618,620],{"class":170,"line":363},[229,619,285],{"class":284},[229,621,622],{"class":238},".message_widget.layout().addWidget(bar)\n",[229,624,625],{"class":170,"line":376},[229,626,265],{"emptyLinePlaceholder":264},[229,628,629,632,634],{"class":170,"line":389},[229,630,631],{"class":238},"    iface.messageBar().pushWidget(",[229,633,304],{"class":284},[229,635,636],{"class":238},".message_widget, Qgis.Info)\n",[229,638,639],{"class":170,"line":401},[229,640,265],{"emptyLinePlaceholder":264},[229,642,643,645,647,649,651,653,655,657],{"class":170,"line":406},[229,644,285],{"class":284},[229,646,288],{"class":238},[229,648,291],{"class":234},[229,650,294],{"class":238},[229,652,298],{"class":297},[229,654,301],{"class":238},[229,656,304],{"class":284},[229,658,307],{"class":238},[229,660,661,663],{"class":170,"line":417},[229,662,285],{"class":284},[229,664,665],{"class":238},".task.progressChanged.connect(bar.setValue)\n",[229,667,668,670,672,674],{"class":170,"line":422},[229,669,285],{"class":284},[229,671,381],{"class":238},[229,673,304],{"class":284},[229,675,676],{"class":238},".clear_message)\n",[229,678,679,681,683,685],{"class":170,"line":433},[229,680,285],{"class":284},[229,682,394],{"class":238},[229,684,304],{"class":284},[229,686,676],{"class":238},[229,688,689,691,693],{"class":170,"line":446},[229,690,409],{"class":238},[229,692,304],{"class":284},[229,694,414],{"class":238},[229,696,697],{"class":170,"line":451},[229,698,265],{"emptyLinePlaceholder":264},[229,700,701,703,706],{"class":170,"line":461},[229,702,271],{"class":234},[229,704,705],{"class":274}," clear_message",[229,707,278],{"class":238},[229,709,710,713,715],{"class":170,"line":472},[229,711,712],{"class":238},"    iface.messageBar().popWidget(",[229,714,304],{"class":284},[229,716,717],{"class":238},".message_widget)\n",[229,719,721,724,727,729,732,734,738,740,743,746,748,750],{"class":170,"line":720},23,[229,722,723],{"class":238},"    iface.messageBar().pushMessage(",[229,725,726],{"class":297},"\"Done\"",[229,728,301],{"class":238},[229,730,731],{"class":297},"\"Areas summed\"",[229,733,301],{"class":238},[229,735,737],{"class":736},"s9osk","level",[229,739,291],{"class":234},[229,741,742],{"class":238},"Qgis.Success, ",[229,744,745],{"class":736},"duration",[229,747,291],{"class":234},[229,749,69],{"class":284},[229,751,330],{"class":238},[14,753,754,487,756,759,760,763,764,767,768,770,771,774],{},[195,755,486],{},[27,757,758],{},"createMessage()"," returns an item whose layout accepts arbitrary widgets, so the progress bar sits inline with the text. ",[27,761,762],{},"pushWidget()"," keeps it visible until removed, unlike ",[27,765,766],{},"pushMessage()"," which fades. Connecting ",[27,769,490],{}," straight to ",[27,772,773],{},"bar.setValue"," works because both are Qt objects on the main thread — no wrapper slot needed. Popping the widget in a handler wired to both completion signals prevents the message bar from accumulating stale progress items, which is what happens when only the success path cleans up.",[184,776,778],{"id":777},"cancel-promptly-not-eventually","Cancel promptly, not eventually",[14,780,781],{},"Cancellation is cooperative. The flag is set instantly; how quickly the work stops is entirely down to how often you look.",[220,783,785],{"className":222,"code":784,"language":224,"meta":225,"style":225},"def run(self):\n    count = self.source.featureCount() or 1\n    for index, feature in enumerate(self.source.getFeatures()):\n        if self.isCanceled():\n            return False                      # checked every feature — stops in milliseconds\n        self.process(feature)\n        if index % 500 == 0:\n            self.setProgress(100 * index \u002F count)\n    return True\n",[27,786,787,796,815,837,847,859,867,889,910],{"__ignoreMap":225},[229,788,789,791,794],{"class":170,"line":231},[229,790,271],{"class":234},[229,792,793],{"class":274}," run",[229,795,278],{"class":238},[229,797,798,801,803,806,809,812],{"class":170,"line":248},[229,799,800],{"class":238},"    count ",[229,802,291],{"class":234},[229,804,805],{"class":284}," self",[229,807,808],{"class":238},".source.featureCount() ",[229,810,811],{"class":234},"or",[229,813,814],{"class":284}," 1\n",[229,816,817,820,823,826,829,832,834],{"class":170,"line":261},[229,818,819],{"class":234},"    for",[229,821,822],{"class":238}," index, feature ",[229,824,825],{"class":234},"in",[229,827,828],{"class":284}," enumerate",[229,830,831],{"class":238},"(",[229,833,304],{"class":284},[229,835,836],{"class":238},".source.getFeatures()):\n",[229,838,839,842,844],{"class":170,"line":268},[229,840,841],{"class":234},"        if",[229,843,805],{"class":284},[229,845,846],{"class":238},".isCanceled():\n",[229,848,849,852,855],{"class":170,"line":281},[229,850,851],{"class":234},"            return",[229,853,854],{"class":284}," False",[229,856,858],{"class":857},"sjoCn","                      # checked every feature — stops in milliseconds\n",[229,860,861,864],{"class":170,"line":310},[229,862,863],{"class":284},"        self",[229,865,866],{"class":238},".process(feature)\n",[229,868,869,871,874,877,880,883,886],{"class":170,"line":315},[229,870,841],{"class":234},[229,872,873],{"class":238}," index ",[229,875,876],{"class":234},"%",[229,878,879],{"class":284}," 500",[229,881,882],{"class":234}," ==",[229,884,885],{"class":284}," 0",[229,887,888],{"class":238},":\n",[229,890,891,894,897,899,902,904,907],{"class":170,"line":333},[229,892,893],{"class":284},"            self",[229,895,896],{"class":238},".setProgress(",[229,898,327],{"class":284},[229,900,901],{"class":234}," *",[229,903,873],{"class":238},[229,905,906],{"class":234},"\u002F",[229,908,909],{"class":238}," count)\n",[229,911,912,915],{"class":170,"line":345},[229,913,914],{"class":234},"    return",[229,916,917],{"class":284}," True\n",[14,919,920,922,923,926,927,930,931,933,934,936,937,939,940,201],{},[195,921,486],{}," The check is per feature while the progress update is every five hundred, and the asymmetry is deliberate: ",[27,924,925],{},"isCanceled()"," is a cheap atomic read, whereas ",[27,928,929],{},"setProgress()"," emits a cross-thread signal. If each iteration is itself slow — a Processing algorithm per feature, a network request — check inside that step too, by passing a feedback object whose ",[27,932,925],{}," is chained to the task's. Returning ",[27,935,479],{}," is what tells the manager the task did not complete, which in turn is what fires ",[27,938,501],{}," rather than ",[27,941,497],{},[14,943,944],{},[39,945,948,951,954,957,960,966,971,977,985,991,994,999,1004,1008,1013,1018],{"viewBox":946,"role":42,"ariaLabel":947,"xmlns":44},"0 0 760 244","Timeline showing the delay between a user pressing cancel and work actually stopping, for a check every feature, a check every thousand features, and no check at all",[46,949,950],{},"How long cancel takes to take effect",[50,952,953],{},"Three bars measure the gap between the cancel press and the task stopping. Checking every feature stops almost immediately. Checking once every thousand features leaves a visible pause. Never checking means the job runs to completion and the cancel button did nothing at all.",[54,955],{"x":56,"y":56,"width":57,"height":956,"fill":59},"244",[78,958,959],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"The flag is instant; the stop is only as fast as your loop",[170,961],{"x1":962,"y1":963,"x2":962,"y2":964,"stroke":965,"style":114},"228","56","196","#b91c1c",[78,967,970],{"x":962,"y":968,"style":969,"fill":965,"textAnchor":84},"216","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","cancel pressed",[78,972,976],{"x":968,"y":973,"style":974,"fill":76,"textAnchor":975},"82","text-anchor:end;font-size:11px;font-family:sans-serif","end","every feature",[54,978],{"x":979,"y":980,"width":981,"height":982,"rx":983,"fill":138,"fillOpacity":984,"stroke":138,"style":114},"230","66","18","24","3",0.4,[78,986,990],{"x":987,"y":988,"style":989,"fill":138},"262","83","font-size:11px;font-family:sans-serif","stops at once",[78,992,993],{"x":968,"y":117,"style":974,"fill":76,"textAnchor":975},"every 1 000",[54,995],{"x":979,"y":996,"width":997,"height":982,"rx":983,"fill":155,"fillOpacity":998,"stroke":155,"style":114},"114","150",0.35,[78,1000,1003],{"x":1001,"y":1002,"style":989,"fill":155},"394","131","a noticeable pause",[78,1005,1007],{"x":968,"y":1006,"style":974,"fill":76,"textAnchor":975},"178","never checked",[54,1009],{"x":979,"y":1010,"width":1011,"height":982,"rx":983,"fill":965,"fillOpacity":1012,"stroke":965,"style":114},"162","472",0.3,[78,1014,1017],{"x":1015,"y":1016,"style":123,"fill":965,"textAnchor":84},"466","179","runs to the end — the button was decoration",[78,1019,1021],{"x":80,"y":1020,"style":123,"fill":132,"textAnchor":84},"236","isCanceled() is a cheap atomic read — checking it every iteration costs nothing measurable",[184,1023,1025],{"id":1024},"leave-the-interface-consistent","Leave the interface consistent",[14,1027,1028],{},"Whatever happens, the dialog has to end up in a state the user can act on. Three rules cover it.",[14,1030,1031,1034],{},[195,1032,1033],{},"Disable the start button while running."," Two concurrent tasks writing the same output is a bug the user should not be able to trigger.",[14,1036,1037,1040],{},[195,1038,1039],{},"Re-enable everything from a single handler"," wired to both completion signals, so there is one place responsible for the post-run state rather than three that can disagree.",[14,1042,1043,1046],{},[195,1044,1045],{},"Guard against the dialog being gone."," If the user closed it, the widgets may be deleted while the task still runs. Check before touching them:",[220,1048,1050],{"className":222,"code":1049,"language":224,"meta":225,"style":225},"from qgis.PyQt import sip\n\ndef on_progress(self, value):\n    if self.dlg is None or sip.isdeleted(self.dlg):\n        return\n    self.dlg.progressBar.setValue(int(value))\n",[27,1051,1052,1064,1068,1076,1103,1108],{"__ignoreMap":225},[229,1053,1054,1056,1059,1061],{"class":170,"line":231},[229,1055,235],{"class":234},[229,1057,1058],{"class":238}," qgis.PyQt ",[229,1060,242],{"class":234},[229,1062,1063],{"class":238}," sip\n",[229,1065,1066],{"class":170,"line":248},[229,1067,265],{"emptyLinePlaceholder":264},[229,1069,1070,1072,1074],{"class":170,"line":261},[229,1071,271],{"class":234},[229,1073,427],{"class":274},[229,1075,430],{"class":238},[229,1077,1078,1081,1083,1086,1089,1092,1095,1098,1100],{"class":170,"line":268},[229,1079,1080],{"class":234},"    if",[229,1082,805],{"class":284},[229,1084,1085],{"class":238},".dlg ",[229,1087,1088],{"class":234},"is",[229,1090,1091],{"class":284}," None",[229,1093,1094],{"class":234}," or",[229,1096,1097],{"class":238}," sip.isdeleted(",[229,1099,304],{"class":284},[229,1101,1102],{"class":238},".dlg):\n",[229,1104,1105],{"class":170,"line":281},[229,1106,1107],{"class":234},"        return\n",[229,1109,1110,1112,1114,1116],{"class":170,"line":310},[229,1111,285],{"class":284},[229,1113,338],{"class":238},[229,1115,440],{"class":284},[229,1117,443],{"class":238},[14,1119,1120,487,1122,1125,1126,1129],{},[195,1121,486],{},[27,1123,1124],{},"sip.isdeleted()"," asks whether the underlying C++ object has been destroyed while the Python wrapper still exists — the exact situation created by a closed dialog, and the cause of \"wrapped C\u002FC++ object has been deleted\" tracebacks appearing minutes after the user closed a window. Returning early is enough; the task continues and reports through the message bar or the log instead. Cancelling the task in the dialog's ",[27,1127,1128],{},"closeEvent"," is the stricter alternative when the work has no value without the dialog.",[184,1131,1133],{"id":1132},"report-stages-not-just-a-percentage","Report stages, not just a percentage",[14,1135,1136],{},"A bar creeping from 0 to 100 over two minutes tells the user how far along the work is but nothing about what it is doing — and when it pauses at 40%, they cannot tell whether it is thinking or stuck.",[220,1138,1140],{"className":222,"code":1139,"language":224,"meta":225,"style":225},"from qgis.PyQt.QtCore import pyqtSignal\n\nclass ExportTask(QgsTask):\n    stageChanged = pyqtSignal(str)\n\n    STAGES = [\"Reading source\", \"Reprojecting\", \"Buffering\", \"Writing output\"]\n\n    def run(self):\n        for index, stage in enumerate(self.STAGES):\n            if self.isCanceled():\n                return False\n            self.stageChanged.emit(f\"{stage} ({index + 1} of {len(self.STAGES)})\")\n            self.setProgress(100 * index \u002F len(self.STAGES))\n            self.do_stage(index)\n        return True\n",[27,1141,1142,1153,1157,1172,1187,1191,1223,1227,1236,1259,1268,1276,1336,1364,1371],{"__ignoreMap":225},[229,1143,1144,1146,1148,1150],{"class":170,"line":231},[229,1145,235],{"class":234},[229,1147,531],{"class":238},[229,1149,242],{"class":234},[229,1151,1152],{"class":238}," pyqtSignal\n",[229,1154,1155],{"class":170,"line":248},[229,1156,265],{"emptyLinePlaceholder":264},[229,1158,1159,1162,1165,1167,1169],{"class":170,"line":261},[229,1160,1161],{"class":234},"class",[229,1163,1164],{"class":274}," ExportTask",[229,1166,831],{"class":238},[229,1168,29],{"class":274},[229,1170,1171],{"class":238},"):\n",[229,1173,1174,1177,1179,1182,1185],{"class":170,"line":268},[229,1175,1176],{"class":238},"    stageChanged ",[229,1178,291],{"class":234},[229,1180,1181],{"class":238}," pyqtSignal(",[229,1183,1184],{"class":284},"str",[229,1186,330],{"class":238},[229,1188,1189],{"class":170,"line":281},[229,1190,265],{"emptyLinePlaceholder":264},[229,1192,1193,1196,1199,1202,1205,1207,1210,1212,1215,1217,1220],{"class":170,"line":310},[229,1194,1195],{"class":284},"    STAGES",[229,1197,1198],{"class":234}," =",[229,1200,1201],{"class":238}," [",[229,1203,1204],{"class":297},"\"Reading source\"",[229,1206,301],{"class":238},[229,1208,1209],{"class":297},"\"Reprojecting\"",[229,1211,301],{"class":238},[229,1213,1214],{"class":297},"\"Buffering\"",[229,1216,301],{"class":238},[229,1218,1219],{"class":297},"\"Writing output\"",[229,1221,1222],{"class":238},"]\n",[229,1224,1225],{"class":170,"line":315},[229,1226,265],{"emptyLinePlaceholder":264},[229,1228,1229,1232,1234],{"class":170,"line":333},[229,1230,1231],{"class":234},"    def",[229,1233,793],{"class":274},[229,1235,278],{"class":238},[229,1237,1238,1241,1244,1246,1248,1250,1252,1254,1257],{"class":170,"line":345},[229,1239,1240],{"class":234},"        for",[229,1242,1243],{"class":238}," index, stage ",[229,1245,825],{"class":234},[229,1247,828],{"class":284},[229,1249,831],{"class":238},[229,1251,304],{"class":284},[229,1253,201],{"class":238},[229,1255,1256],{"class":284},"STAGES",[229,1258,1171],{"class":238},[229,1260,1261,1264,1266],{"class":170,"line":358},[229,1262,1263],{"class":234},"            if",[229,1265,805],{"class":284},[229,1267,846],{"class":238},[229,1269,1270,1273],{"class":170,"line":363},[229,1271,1272],{"class":234},"                return",[229,1274,1275],{"class":284}," False\n",[229,1277,1278,1280,1283,1286,1289,1292,1295,1298,1301,1303,1306,1309,1312,1315,1318,1320,1322,1324,1326,1329,1331,1334],{"class":170,"line":376},[229,1279,893],{"class":284},[229,1281,1282],{"class":238},".stageChanged.emit(",[229,1284,1285],{"class":234},"f",[229,1287,1288],{"class":297},"\"",[229,1290,1291],{"class":284},"{",[229,1293,1294],{"class":238},"stage",[229,1296,1297],{"class":284},"}",[229,1299,1300],{"class":297}," (",[229,1302,1291],{"class":284},[229,1304,1305],{"class":238},"index ",[229,1307,1308],{"class":234},"+",[229,1310,1311],{"class":284}," 1}",[229,1313,1314],{"class":297}," of ",[229,1316,1317],{"class":284},"{len",[229,1319,831],{"class":238},[229,1321,304],{"class":284},[229,1323,201],{"class":238},[229,1325,1256],{"class":284},[229,1327,1328],{"class":238},")",[229,1330,1297],{"class":284},[229,1332,1333],{"class":297},")\"",[229,1335,330],{"class":238},[229,1337,1338,1340,1342,1344,1346,1348,1350,1353,1355,1357,1359,1361],{"class":170,"line":389},[229,1339,893],{"class":284},[229,1341,896],{"class":238},[229,1343,327],{"class":284},[229,1345,901],{"class":234},[229,1347,873],{"class":238},[229,1349,906],{"class":234},[229,1351,1352],{"class":284}," len",[229,1354,831],{"class":238},[229,1356,304],{"class":284},[229,1358,201],{"class":238},[229,1360,1256],{"class":284},[229,1362,1363],{"class":238},"))\n",[229,1365,1366,1368],{"class":170,"line":401},[229,1367,893],{"class":284},[229,1369,1370],{"class":238},".do_stage(index)\n",[229,1372,1373,1376],{"class":170,"line":406},[229,1374,1375],{"class":234},"        return",[229,1377,917],{"class":284},[14,1379,1380,1382,1383,1386],{},[195,1381,486],{}," A custom signal carrying a string is the simplest way to say what is happening; connected to a label or the message bar item's text it costs nothing and removes most \"is it stuck?\" questions. Emitting from ",[27,1384,1385],{},"run()"," on a worker thread is safe because Qt queues the signal to the receiving thread's event loop — the slot runs on the main thread, where touching a widget is legal. Counting stages in the text (\"3 of 4\") gives the user a sense of scale that a percentage alone does not, particularly when the stages differ wildly in duration.",[14,1388,1389],{},"Resist the temptation to estimate a remaining time from elapsed progress. Geospatial work is rarely uniform — a buffer over dense urban geometry takes many times longer per feature than one over farmland — so a naive extrapolation is usually wrong in the direction that annoys people most. A stage name and an honest percentage age better than a confident and incorrect countdown.",[14,1391,1392],{},[39,1393,1396,1399,1402,1404,1407,1412,1419,1422,1427,1432,1436,1439,1441,1444,1447],{"viewBox":1394,"role":42,"ariaLabel":1395,"xmlns":44},"0 0 760 236","A four-stage progress display showing the current stage name and position alongside the bar, compared with a bare percentage that stalls without explanation",[46,1397,1398],{},"A stage name explains a bar that has stopped moving",[50,1400,1401],{},"The upper display is a bare progress bar sitting at forty percent with no indication of what is happening. The lower display shows the same bar with the current stage named and numbered, so the pause is legible as a long writing step rather than as a hang.",[54,1403],{"x":56,"y":56,"width":57,"height":1020,"fill":59},[78,1405,1406],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"Same progress, one of them answers the obvious question",[78,1408,1411],{"x":982,"y":1409,"style":1410,"fill":155},"80","font-size:12px;font-weight:bold;font-family:sans-serif","bar only",[54,1413],{"x":174,"y":1414,"width":1415,"height":1416,"rx":69,"fill":1417,"stroke":132,"style":1418},"62","440","26","#fffdf7","stroke-width:1.5",[54,1420],{"x":174,"y":1414,"width":1421,"height":1416,"rx":69,"fill":155,"fillOpacity":998,"stroke":155,"style":1418},"176",[78,1423,1426],{"x":1424,"y":1425,"style":989,"fill":76},"644","81","40%",[78,1428,1431],{"x":1429,"y":1430,"style":123,"fill":155,"textAnchor":84},"400","112","stuck, or slow? the user cannot tell",[78,1433,1435],{"x":982,"y":1434,"style":1410,"fill":138},"164","with stages",[54,1437],{"x":174,"y":1438,"width":1415,"height":1416,"rx":69,"fill":1417,"stroke":132,"style":1418},"146",[54,1440],{"x":174,"y":1438,"width":1421,"height":1416,"rx":69,"fill":138,"fillOpacity":998,"stroke":138,"style":1418},[78,1442,1426],{"x":1424,"y":1443,"style":989,"fill":76},"165",[78,1445,1446],{"x":1429,"y":964,"style":123,"fill":138,"textAnchor":84},"Writing output (4 of 4) — the slow step, and it is running",[78,1448,1450],{"x":80,"y":1449,"style":123,"fill":132,"textAnchor":84},"224","A stage name ages better than an estimated time that assumes uniform work",[184,1452,1454],{"id":1453},"qgis-version-compatibility","QGIS version compatibility",[14,1456,1457,1458,1460],{},"The examples target ",[195,1459,197],{}," (Python 3.12).",[1462,1463,1464,1480],"table",{},[1465,1466,1467],"thead",{},[1468,1469,1470,1474,1477],"tr",{},[1471,1472,1473],"th",{},"QGIS version",[1471,1475,1476],{},"Python",[1471,1478,1479],{},"Notes",[1481,1482,1483,1499,1509,1520],"tbody",{},[1468,1484,1485,1489,1492],{},[1486,1487,1488],"td",{},"3.22 LTR",[1486,1490,1491],{},"3.9",[1486,1493,1494,1495,1498],{},"All signals present; ",[27,1496,1497],{},"Qgis.MessageLevel"," members named identically.",[1468,1500,1501,1504,1506],{},[1486,1502,1503],{},"3.28 LTR",[1486,1505,1491],{},[1486,1507,1508],{},"Identical behaviour.",[1468,1510,1511,1514,1517],{},[1486,1512,1513],{},"3.34 LTR",[1486,1515,1516],{},"3.12",[1486,1518,1519],{},"Baseline for this page.",[1468,1521,1522,1525,1527],{},[1486,1523,1524],{},"3.40 \u002F 3.44",[1486,1526,1516],{},[1486,1528,1529,1530,1533],{},"Qt enum members should be written scoped (",[27,1531,1532],{},"Qt.AlignmentFlag.AlignLeft",") for forward compatibility with Qt 6 builds.",[184,1535,1537],{"id":1536},"troubleshooting","Troubleshooting",[189,1539,1540,1556,1568,1577,1586,1592],{},[192,1541,1542,487,1545,1547,1548,1551,1552,1555],{},[195,1543,1544],{},"The bar never moves.",[27,1546,929],{}," is not being called, or it is called with a value derived from ",[27,1549,1550],{},"featureCount()"," returning ",[27,1553,1554],{},"-1",". Guard the divisor.",[192,1557,1558,1561,1562,1564,1565,1567],{},[195,1559,1560],{},"The dialog stays in the running state after a cancel."," Only ",[27,1563,497],{}," was connected. Wire ",[27,1566,501],{}," to the same handler.",[192,1569,1570,1573,1574,1576],{},[195,1571,1572],{},"\"wrapped C\u002FC++ object has been deleted\"."," A slot touched a widget after its dialog closed. Guard with ",[27,1575,1124],{}," or cancel on close.",[192,1578,1579,487,1582,1585],{},[195,1580,1581],{},"The message bar fills with old progress items.",[27,1583,1584],{},"popWidget()"," is only called on the success path.",[192,1587,1588,1591],{},[195,1589,1590],{},"Cancel takes many seconds."," The loop checks the flag too rarely, or one iteration is itself long. Chain a feedback object into any inner algorithm call.",[192,1593,1594,1597],{},[195,1595,1596],{},"The progress bar jumps backwards."," Two tasks are connected to the same bar. Give each run its own widget, or refuse to start while one is running.",[184,1599,1601],{"id":1600},"conclusion","Conclusion",[14,1603,1604,1605,1607,1608,498,1610,1612,1613,1615],{},"Progress and cancellation are four connections and one discipline: ",[27,1606,490],{}," to a bar, both ",[27,1609,497],{},[27,1611,501],{}," to a single cleanup handler, ",[27,1614,925],{}," checked every iteration, and every widget touch guarded against a dialog that may already be gone. Prefer the message bar for anything long enough that the user will go and do something else.",[184,1617,1619],{"id":1618},"frequently-asked-questions","Frequently Asked Questions",[14,1621,1622,1625],{},[195,1623,1624],{},"Do I need any of this if the task manager already shows progress?","\nFor a short job, no — the panel is free and includes a cancel button. For anything the user is waiting on, put the feedback where they are already looking.",[14,1627,1628,1631,1632,1634],{},[195,1629,1630],{},"How do I show progress for work with no countable steps?","\nSet the progress bar's range to 0–0, which renders an indeterminate animation, and leave ",[27,1633,929],{}," alone. Prefer a real count wherever one exists.",[14,1636,1637,1640,1641,1644,1645,1647],{},[195,1638,1639],{},"Can I cancel a Processing algorithm running inside my task?","\nYes — pass a ",[27,1642,1643],{},"QgsProcessingFeedback"," whose ",[27,1646,925],{}," reflects the task's, and the algorithm will stop at its next check.",[14,1649,1650,1653],{},[195,1651,1652],{},"Should the cancel button ask for confirmation?","\nNo. Cancel should be immediate; a confirmation dialog on an operation the user has already decided to abandon is friction, and partial results should simply be discarded.",[14,1655,1656,1659,1660,1663],{},[195,1657,1658],{},"How do I report which stage a long job is in?","\nUpdate the message bar item's text, or emit a custom signal carrying a stage name and connect it to a label. Both are delivered on the main thread when emitted from ",[27,1661,1662],{},"finished()"," or via a queued connection.",[184,1665,1667],{"id":1666},"related","Related",[189,1669,1670,1675,1679,1685,1691],{},[192,1671,1672,1674],{},[21,1673,24],{"href":23}," — the guide this recipe belongs to",[192,1676,1677],{},[21,1678,34],{"href":33},[192,1680,1681],{},[21,1682,1684],{"href":1683},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fadd-custom-dock-widget-pyqgis\u002F","Add a Custom Dock Widget in PyQGIS",[192,1686,1687],{},[21,1688,1690],{"href":1689},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fload-ui-file-at-runtime-pyqgis\u002F","Load a .ui File at Runtime in PyQGIS",[192,1692,1693],{},[21,1694,208],{"href":207},[1696,1697,1698],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":225,"searchDepth":248,"depth":248,"links":1700},[1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711],{"id":186,"depth":248,"text":187},{"id":217,"depth":248,"text":218},{"id":505,"depth":248,"text":506},{"id":777,"depth":248,"text":778},{"id":1024,"depth":248,"text":1025},{"id":1132,"depth":248,"text":1133},{"id":1453,"depth":248,"text":1454},{"id":1536,"depth":248,"text":1537},{"id":1600,"depth":248,"text":1601},{"id":1618,"depth":248,"text":1619},{"id":1666,"depth":248,"text":1667},"Give plugin users feedback and control — drive a progress bar from a QgsTask, use the message bar for long jobs, honour cancellation promptly, and keep the dialog consistent when work ends.","md",{"slug":1715,"type":1716,"breadcrumb":1717,"datePublished":1718,"dateModified":1718},"show-plugin-progress-and-cancellation-pyqgis","article","Progress & Cancellation","2026-08-10","\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis",{"title":5,"description":1712},"qgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002Findex","3FFSk6WD1pwyZGNCtLgxvXSxmCGktTlExcgJN_nWsPk",1786401337553]