AttributeError: 'MapMarkerApp' object has no attribute 'save_map_state'
时间: 2025-06-09 14:08:06 浏览: 15
<think>我们之前提供的代码中,由于篇幅限制,没有完整地包含`save_map_state`等方法。现在需要为`MapMarkerApp`类补充这些缺失的方法。具体需要补充的方法包括:`save_map_state`,`save_map_state_callback`,`load_map_state`,`load_existing_markers`等。下面我们给出完整的代码,包括之前缺失的方法:</think>
这个错误是因为 `MapMarkerApp` 类中没有定义 `save_map_state` 方法。您需要为类添加这个方法,以及它依赖的其他方法。以下是为您提供的完整解决方案:
```python
import os
import json
import shutil
import sys
import time
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QDialog, QLabel, QLineEdit, QTextEdit, QPushButton,
QFileDialog, QVBoxLayout, QHBoxLayout, QToolBar, QAction, QWidget, QScrollArea, QMessageBox
)
from PyQt5.QtCore import Qt, QObject, pyqtSlot
from PyQt5.QtGui import QPixmap
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel
class BridgeObject(QObject):
"""Python和JavaScript之间的通信桥梁"""
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
@pyqtSlot(str)
def handle_message(self, message):
"""处理来自JavaScript的消息"""
self.parent.handle_js_message(message)
class MapMarkerApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("地图标记工具")
self.setGeometry(100, 100, 1200, 800)
# 创建中央部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# 创建地图视图
self.map_view = QWebEngineView()
layout.addWidget(self.map_view)
# 创建工具栏
self.toolbar = QToolBar("主工具栏")
self.addToolBar(self.toolbar)
# 添加标记模式按钮
self.add_marker_action = QAction("添加标记模式", self)
self.add_marker_action.setCheckable(True)
self.add_marker_action.toggled.connect(self.toggle_add_marker_mode)
self.toolbar.addAction(self.add_marker_action)
# 保存状态按钮
save_action = QAction("保存地图状态", self)
save_action.triggered.connect(self.save_map_state)
self.toolbar.addAction(save_action)
# 状态变量
self.add_mode = False
self.markers = {} # 存储标记数据
# 创建通信桥梁
self.bridge = BridgeObject(self)
# 设置WebChannel通信
self.channel = QWebChannel()
self.channel.registerObject("bridge", self.bridge)
self.map_view.page().setWebChannel(self.channel)
# 加载地图
self.load_map()
self.map_view.loadFinished.connect(self.on_map_loaded)
# 创建必要目录
os.makedirs("markers_data", exist_ok=True)
def load_map(self):
"""加载百度地图"""
# 百度地图API密钥(确保您有有效的API密钥)
baidu_key = "68kdoA24XGfaUwDZD7TviZO9QY0RCb0I"
# 确保qwebchannel.js文件存在
script_dir = os.path.dirname(os.path.abspath(__file__))
qwebchannel_path = os.path.join(script_dir, 'qwebchannel.js')
if not os.path.exists(qwebchannel_path):
try:
from PyQt5.QtWebChannel import QWebChannel
src_dir = os.path.dirname(QWebChannel.__file__)
qwebchannel_src = os.path.join(src_dir, 'qwebchannel.js')
if os.path.exists(qwebchannel_src):
shutil.copyfile(qwebchannel_src, qwebchannel_path)
except Exception as e:
print(f"无法复制qwebchannel.js: {e}")
# 将路径中的反斜杠替换为斜杠
if os.path.exists(qwebchannel_path):
qwebchannel_path_forward = qwebchannel_path.replace('\\', '/')
# 使用拼接方式构建HTML内容
html_content = '''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地图标记</title>
<style>
html, body, #map-container {
width:100%;
height:100%;
margin:0;
padding:0;
overflow: hidden;
}
</style>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=''' + baidu_key + '''"></script>
<script type="text/javascript" src="file:///''' + qwebchannel_path_forward + '''"></script>
</head>
<body>
<div id="map-container"></div>
<script>
// 解决跨域问题
document.domain = document.domain;
// 初始化地图
var map = new BMap.Map("map-container");
var point = new BMap.Point(116.404,39.915); // 默认坐标:北京市中心
map.centerAndZoom(point,12);
map.enableScrollWheelZoom(true);
map.addControl(new BMap.NavigationControl()); // 添加缩放控件
// 存储标记对象
var markers = {};
// 初始化通信通道
new QWebChannel(qt.webChannelTransport, function(channel) {
window.bridge = channel.objects.bridge;
});
// 地图点击事件
map.addEventListener("click", function(e) {
if (window.bridge && window.addMode) {
window.bridge.handle_message(JSON.stringify({
type: 'map_click',
lat: e.point.lat,
lng: e.point.lng
}));
}
});
// 添加标记的方法
function addMarker(lat, lng, id, title, content) {
var point = new BMap.Point(lng, lat);
var marker = new BMap.Marker(point);
map.addOverlay(marker);
// 添加点击事件
marker.addEventListener("click", function() {
if (!window.addMode) {
window.bridge.handle_message(JSON.stringify({
type: 'marker_click',
marker_id: id
}));
}
});
// 添加到全局标记对象
markers[id] = {
marker: marker,
title: title,
content: content
};
}
// 移除标记的方法
function removeMarker(id) {
if (markers[id]) {
map.removeOverlay(markers[id].marker);
delete markers[id];
}
}
// 获取地图状态
function getMapState() {
var center = map.getCenter();
return {
center: { lat: center.lat, lng: center.lng },
zoom: map.getZoom()
};
}
// 设置地图视图
function setMapView(state) {
if (state && state.center) {
var point = new BMap.Point(state.center.lng, state.center.lat);
map.centerAndZoom(point, state.zoom ||15);
}
}
// 暴露方法给Python
window.mapFunctions = {
addMarker: addMarker,
removeMarker: removeMarker,
getMapState: getMapState,
setMapView: setMapView
};
// 初始化模式
window.addMode = false;
// 确保地图加载完成
map.addEventListener("tilesloaded", function() {
console.log("地图加载完成");
if (window.bridge) {
window.bridge.handle_message(JSON.stringify({
type: 'map_loaded'
}));
}
});
</script>
</body>
</html>'''
self.map_view.setHtml(html_content)
else:
QMessageBox.critical(self, "错误", "找不到qwebchannel.js文件,地图无法加载")
def on_map_loaded(self, success):
"""地图加载完成后初始化"""
if success:
print("地图加载成功")
# 等待JavaScript初始化完成
self.map_view.page().runJavaScript(
"if (typeof window.mapFunctions !== 'undefined') true; else false;",
self.init_after_js_ready
)
else:
print("地图加载失败")
QMessageBox.critical(self, "错误", "地图加载失败,请检查网络连接和API密钥")
def init_after_js_ready(self, result):
"""JavaScript环境准备就绪后回调"""
if result:
# 加载地图状态
self.load_map_state()
# 加载已有标记
self.load_existing_markers()
else:
# 延迟重试
time.sleep(0.5)
self.map_view.page().runJavaScript(
"if (typeof window.mapFunctions !== 'undefined') true; else false;",
self.init_after_js_ready
)
# 以下是新增的方法
def toggle_add_marker_mode(self, checked):
"""切换添加标记模式"""
self.add_mode = checked
# 将布尔值转换为字符串发送到JavaScript
js_code = f"window.addMode = {'true' if checked else 'false'};"
self.map_view.page().runJavaScript(js_code)
def handle_js_message(self, message):
"""处理来自JavaScript的消息"""
try:
data = json.loads(message)
if data["type"] == "map_click" and self.add_mode:
self.create_new_marker(data["lat"], data["lng"])
elif data["type"] == "marker_click":
self.show_marker_info(data["marker_id"])
except Exception as e:
print("处理JS消息错误:", e)
print("收到的消息:", message)
def create_new_marker(self, lat, lng):
"""创建新的标记"""
# 生成唯一ID
marker_id = f"marker_{int(time.time() * 1000)}"
# 对话框获取标记信息
dialog = QDialog(self)
dialog.setWindowTitle("添加标记")
layout = QVBoxLayout()
# 标题输入
title_label = QLabel("标题:")
title_input = QLineEdit()
layout.addWidget(title_label)
layout.addWidget(title_input)
# 内容输入
content_label = QLabel("内容:")
content_input = QTextEdit()
layout.addWidget(content_label)
layout.addWidget(content_input)
# 按钮
button_box = QHBoxLayout()
ok_btn = QPushButton("确定")
cancel_btn = QPushButton("取消")
button_box.addWidget(ok_btn)
button_box.addWidget(cancel_btn)
layout.addLayout(button_box)
# 连接按钮信号
def accept():
title = title_input.text().strip()
content = content_input.toPlainText().strip()
if not title:
QMessageBox.warning(dialog, "输入错误", "标题不能为空")
return
dialog.accept()
# 在JavaScript中添加标记
js_code = f"window.mapFunctions.addMarker({lat}, {lng}, '{marker_id}', '{title}', `{content}`);"
self.map_view.page().runJavaScript(js_code)
# 保存标记数据
self.markers[marker_id] = {
"lat": lat,
"lng": lng,
"title": title,
"content": content
}
# 保存标记数据到文件
self.save_marker_to_file(marker_id)
ok_btn.clicked.connect(accept)
cancel_btn.clicked.connect(dialog.reject)
dialog.setLayout(layout)
dialog.exec_()
def show_marker_info(self, marker_id):
"""显示标记的信息"""
if marker_id in self.markers:
marker = self.markers[marker_id]
info_dialog = QDialog(self)
info_dialog.setWindowTitle(marker["title"])
layout = QVBoxLayout()
content_label = QTextEdit(marker["content"])
content_label.setReadOnly(True)
layout.addWidget(content_label)
# 删除按钮
del_btn = QPushButton("删除标记")
del_btn.clicked.connect(lambda: self.delete_marker(marker_id, info_dialog))
layout.addWidget(del_btn)
info_dialog.setLayout(layout)
info_dialog.exec_()
def delete_marker(self, marker_id, dialog):
"""删除标记"""
if marker_id in self.markers:
# 从地图中移除
js_code = f"window.mapFunctions.removeMarker('{marker_id}');"
self.map_view.page().runJavaScript(js_code)
# 从内存中删除
del self.markers[marker_id]
# 删除文件
marker_file = os.path.join("markers_data", f"{marker_id}.json")
if os.path.exists(marker_file):
os.remove(marker_file)
dialog.accept()
def save_map_state(self):
"""保存地图状态"""
# 获取当前地图状态
js_code = "window.mapFunctions.getMapState();"
self.map_view.page().runJavaScript(js_code, self.save_map_state_callback)
def save_map_state_callback(self, state_json):
"""地图状态获取后的回调函数"""
if not state_json:
QMessageBox.warning(self, "错误", "获取地图状态失败")
return
try:
state = json.loads(state_json)
# 保存到文件
with open("map_state.json", "w") as f:
json.dump(state, f)
QMessageBox.information(self, "保存成功", "地图状态已保存")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存地图状态时出错: {str(e)}")
def load_map_state(self):
"""加载保存的地图状态"""
map_state_file = "map_state.json"
if os.path.exists(map_state_file):
try:
with open(map_state_file) as f:
state = json.load(f)
# 设置地图视图
js_code = f"window.mapFunctions.setMapView({json.dumps(state)});"
self.map_view.page().runJavaScript(js_code)
except Exception as e:
print("加载地图状态失败:", e)
def load_existing_markers(self):
"""加载已有的标记数据"""
markers_dir = "markers_data"
if os.path.exists(markers_dir):
for file_name in os.listdir(markers_dir):
if file_name.endswith(".json"):
marker_id = file_name[:-5] # 移除 .json 后缀
marker_file = os.path.join(markers_dir, file_name)
try:
with open(marker_file) as f:
marker_data = json.load(f)
# 将标记添加到内存
self.markers[marker_id] = marker_data
# 在JavaScript中添加标记
js_code = f"window.mapFunctions.addMarker({marker_data['lat']}, {marker_data['lng']}, '{marker_id}', '{marker_data['title']}', `{marker_data['content']}`);"
self.map_view.page().runJavaScript(js_code)
except Exception as e:
print(f"加载标记{marker_id}失败:", e)
def save_marker_to_file(self, marker_id):
"""保存标记数据到文件"""
if marker_id in self.markers:
marker_data = self.markers[marker_id]
marker_file = os.path.join("markers_data", f"{marker_id}.json")
try:
with open(marker_file, "w") as f:
json.dump(marker_data, f)
except Exception as e:
QMessageBox.critical(self, "保存失败", f"无法保存标记数据: {str(e)}")
if __name__ == "__main__":
# 解决高DPI显示问题
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
# 确保当前目录正确
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
main_window = MapMarkerApp()
main_window.show()
sys.exit(app.exec_())
```
### 新增方法说明
1. **`save_map_state` 和 `save_map_state_callback`**
- `save_map_state` 从JavaScript获取当前地图状态
- `save_map_state_callback` 处理获取到的状态数据并保存到文件
2. **`load_map_state`**
- 加载保存的地图状态文件并应用到地图
3. **`create_new_marker`**
- 在用户点击地图时创建新标记
- 显示对话框让用户输入标记信息
4. **`show_marker_info` 和 `delete_marker`**
- 显示标记信息对话框
- 提供删除标记的功能
5. **`save_marker_to_file` 和 `load_existing_markers`**
- 保存标记数据到JSON文件
- 从磁盘加载已有的标记数据并添加到地图
### 功能增强
1. **持久化存储**:标记数据和地图状态被保存到文件
2. **完整标记管理**:用户可以添加、查看和删除标记
3. **UI交互优化**:
- 对话框表单收集标记信息
- 信息展示使用只读文本编辑框
- 错误处理提示
### 修复方案
1. **添加缺失的方法**:实现了所有缺少的方法,包括地图状态保存和标记管理
2. **完善标记生命周期**:标记从创建、展示到删除的完整流程
3. **数据持久化**:使用JSON文件存储地图状态和标记数据
这个完整解决方案不仅修复了 `AttributeError: 'MapMarkerApp' object has no attribute 'save_map_state'` 错误,还提供了完整的地图标记应用功能。
阅读全文
相关推荐





