2

QGIS 3.40.14 with Python 3.12.12

I created a custom QgsCustomDropHandler that reads lines and points from a text file (not .shp or any of the other supported formats) and want to display them on the map:

def handleFileDrop(self, file):
    #Do all the checks (correct format,...) and read lines and points
    print(len(ls),"lines")
    self.vectorlayer = QgsVectorLayer("Point", "mylayer", "memory")
    dataprovider = self.vectorlayer.dataProvider()

    fields = QgsFields()
    fields.append(QgsField('name',QVariant.String))
    fields.append(QgsField('id', QVariant.Int))
    fields.append(QgsField('date', QVariant.Int))

    lines = []

    for l in ls:
        line = QgsFeature(fields)
        points = []

        for p in l.points:
            points.append(QgsPoint(p.x,p.y,p.z))

        line.setGeometry(QgsGeometry.fromPolyline(points))
        line.setAttributes([l.name,l.id,l.date])
        lines.append(line)
        #dataprovider.addFeature(line) #Version 2

    (result, newFeatures) = dataprovider.addFeatures(lines) #Version 1
    print("result:",result,"-",len(newFeatures),"new features")
    self.vectorlayer.updateExtents()
    print(dataprovider.featureCount(),"features")
    QgsProject.instance().addMapLayer(self.vectorlayer)
    self.iface.mapCanvas().refresh()

Extra classes:

@dataclass
class Line:
    name: str
    id: int
    date: int
    points: list[Point]

@dataclass
class Point:
    x: float
    y: float
    z: float
    id: int
    time: int

I adapted the code posted in this answer and I'm aware that this creates a new layer every time a valid file is dragged into QGIS (this is fine for now). The layer is created, there aren't any errors but nothing is drawn on the map.

Prints:

1 lines
result: False - 1 new features
0 features

So it does successfully find and read one line from the file (~ 50 points) but for some reason actually adding it fails. It doesn't matter if I use version 1 or 2, nothing is ever displayed on the map and right-click "show feature count" is always "[0]".

I also tested the version without QgsVectorDataProvider, as suggested here (I don't need any signals):

with edit(self.vectorlayer):
    self.vectorlayer.addFeatures(lines)

result = self.vectorlayer.updateExtents()
print("result:",result)

This always prints: result: None

What am I missing?

Bonus question: Is my way of adding attributes for each line correct? There's probably no way to also add information to specific points (like the id or time), if it's even possible to select them in a polyline, is there?

Edit:

I can successfully add a single point with features:

fields = QgsFields()
fields.append(QgsField('id', QVariant.Int))
fields.append(QgsField('time', QVariant.Int))
dataprovider.addAttributes(fields)
self.vectorlayer.updateFields()

p1 = ls[0].points[0]

feature = QgsFeature(fields)
feature.setGeometry(QgsGeometry.fromPoint(QgsPoint(p1.x,p1.y,p1.z)))
feature.setAttributes([p1.id, p1.time])
(result, newFeatures) = dataprovider.addFeatures([feature])
print("result:",result,"-",len(newFeatures),"new features")
self.vectorlayer.updateExtents()
print(dataprovider.featureCount(),"features")

... and also all points within that line (1 point=1 feature in a list) but for some reason it always fails when I want to add a full polyline. Does QgsGeometry.fromPolyline require additional code (or something like that)?

3
  • 1
    One obvious problem I notice is that you define 3 fields but never add them to the layer with e.g. dataprovider.addAttributes(fields) then self.vectorlayer.updateFields(). If the feature attributes don't match the layer fields, the feature/s will not be successfully added. Commented yesterday
  • @BenW Changed it, thanks! Sadly it's still not working: It still only returns False and afterwards there aren't any features in the layer. Commented 20 hours ago
  • Did some more testing (see question for the code): It works fine if I use a feature per point but for some reason it doesn't work when a polyline is a feature (or maybe something in QgsGeometry.fromPolyline is causing the problem?). Commented 19 hours ago

1 Answer 1

3

As far as I can see there are several problems in your code

Issue 1: Geometry/Layer type mismatch

Your layer is declared as "Point" but you're adding QgsGeometry.fromPolyline(...) geometries to it. QGIS silently rejects features whose geometry type doesn't match the layer type, hence result: False with no error message.

# Declares a LineString (polyline) layer
self.vectorlayer = QgsVectorLayer("LineString", "mylayer", "memory")

# If your points have Z coordinates (which yours do), use:
self.vectorlayer = QgsVectorLayer("LineStringZ", "mylayer", "memory")

Issue 2: Fields are never registered with the provider

In your working point example you noticed the fix yourself. You must call dataprovider.addAttributes() and self.vectorlayer.updateFields() before adding features. Without this the fields only exist on the QgsFields object you pass to QgsFeature, but the layer's data provider doesn't know about them, so attribute writes silently fail too.

def handleFileDrop(self, file):
    # read your data 
    print(len(ls), "lines")

    # 1. Create as LineStringZ (matches QgsPoint with x, y, z)
    self.vectorlayer = QgsVectorLayer("LineStringZ", "mylayer", "memory")
    dataprovider = self.vectorlayer.dataProvider()

    # 2. Register fields with the provider first
    dataprovider.addAttributes([
        QgsField('name', QVariant.String),
        QgsField('id',   QVariant.Int),
        QgsField('date', QVariant.Int),
    ])
    self.vectorlayer.updateFields()          # sync layer & provider

    # 3. Build features using the layer's own field definition
    fields = self.vectorlayer.fields()
    features = []

    for l in ls:
        feat = QgsFeature(fields)
        pts  = [QgsPoint(p.x, p.y, p.z) for p in l.points]
        feat.setGeometry(QgsGeometry.fromPolyline(pts))
        feat.setAttributes([l.name, l.id, l.date])
        features.append(feat)

    result, new_feats = dataprovider.addFeatures(features)
    print("result:", result, "-", len(new_feats), "new features")

    self.vectorlayer.updateExtents()
    print(dataprovider.featureCount(), "features")

    QgsProject.instance().addMapLayer(self.vectorlayer)
    self.iface.mapCanvas().refresh()

Note: A polyline is a single feature, its vertices are just coordinates with no attribute table rows of their own, so there's no standard way to attach id/time per vertex inside a LineString layer.


You still go through a provider, that hasn't changed. What has changed is the preferred pattern. Rather than keeping a QgsVectorDataProvider reference around and calling startEditing()/commitChanges() manually, the modern idiomatic approaches are:

Option A: with edit context manager

layer = QgsVectorLayer("Point?crs=EPSG:4326&field=name:string", "my_layer", "memory")

# Access the provider once to define fields — no need to keep the reference
layer.dataProvider().addAttributes([QgsField("name", QVariant.String)])
layer.updateFields()

with edit(layer):  # handles startEditing + commitChanges (+ rollback on error)
    feature = QgsFeature(layer.fields())
    feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(8.0, 49.0)))
    feature.setAttribute("name", "test")
    layer.addFeature(feature)

with edit calls startEditing() on enter and commitChanges() on exit, and rolls back cleanly if an exception is raised. No manual bookkeeping needed.

Option B: dataProvider().addFeatures() directly

It bypasses the edit buffer entirely and is faster for bulk-loading a scratch layer you're not going to edit interactively:

layer.dataProvider().addFeatures([feature1, feature2])
layer.updateExtents()

For a temporary/memory layer you're just populating once, Option B is fine. For layers where you want undo/redo support or signals to fire properly, use with edit.


QgsVectorLayer(..., "memory") vs QgsMemoryProviderUtils.createMemoryLayer()

Functionally they produce the same thing - a memory-backed layer. The difference is ergonomics:

QgsVectorLayer("Point?field=...", name, "memory") QgsMemoryProviderUtils.createMemoryLayer(name, fields, geomType, crs)
Field definition URI string ("field=name:string:0:0") Pass a proper QgsFields object
CRS In the URI or set after Passed directly
Readability Gets unwieldy with many fields Cleaner for complex schemas
Use case Quick/simple layers Programmatic layer creation with existing QgsFields

So it's mostly a convenience wrapper, useful when you already have a QgsFields object (e.g. copied from another layer).


Connecting Points While Keeping Them Interactive

A few options depending on your use case:

  1. Two layers: A Point layer with all your attributes and interactivity, plus a virtual LineString layer purely as a visual connector.

  2. Geometry Generator symbology: Keep only your Point layer and add a Geometry Generator symbol layer to it. You can write an expression that draws a line connecting the points dynamically, zero extra data management.

  3. QgsCompoundCurve / QgsCircularString: These let you build geometrically connected curves, but they're single-feature geometries, so you'd lose per-point attribute storage and individual interactivity. Probably not what you want here.

For most use cases, the Geometry Generator approach: all your data lives in one place, points remain individually selectable and attribute-bearing, and the connecting line is just a rendering artefact.


References:

4
  • I changed it to LineStringZ and now it's working, thanks! Is it true that you should not use DataProviders anymore and should instead set the features on the layer directly? Is there any kind of collection (or line) that connects the points but still makes it possibe to interact with them separately and to set attributes? Commented 17 hours ago
  • I found an earlier version of Joseph's answer (see link in my question) with with edit. So: Only access the QgsVectorDataProvider to set the layer attribute fields, without keeping a reference, then use with edit on the layer (no extra startEditing and commitChanges needed) to add the features? What's the difference between QgsMemoryProviderUtils.createMemoryLayer() and using QgsVectorLayer directly? Only that it creates a memory layer automatically? Commented 16 hours ago
  • The points are pretty much in a straight line (with a little bit of offset). I was thinking about doing two layers, which is easy enough to do, as I already read the file, but it requires managing two layers and you have to switch. Can you also add attributes to the connections while using a Geometry Generator? Commented 16 hours ago
  • Thanks for the additional explanations! If I want to add attributes to the connecting line too, then I need a "real" layer (not a virtual one or Geometry Generator), correct? (This probably has the additional benefit of being able to export the line too.) Commented 14 hours ago

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.