PYcharm------图形化天气组件

免费的天气API网站,注册订阅即可免费使用
python
import sys
import requests
import json
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QTextEdit, QComboBox, QGroupBox, QProgressBar,
QMessageBox, QSplitter, QCheckBox)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFont
class ApiWorker(QThread):
"""API请求工作线程"""
finished = pyqtSignal(dict)
error = pyqtSignal(str)
def __init__(self, api_key, city, country_code, lang='zh_cn', original_city=None):
super().__init__()
self.api_key = api_key
self.city = city
self.country_code = country_code
self.lang = lang
self.original_city = original_city # 保存原始中文城市名
def run(self):
try:
base_url = "http://api.openweathermap.org/data/2.5/weather"
params = {
'q': f'{self.city},{self.country_code}',
'appid': self.api_key,
'units': 'metric',
'lang': self.lang
}
response = requests.get(base_url, params=params, timeout=10)
result = {
'status_code': response.status_code,
'data': response.json() if response.status_code == 200 else None,
'raw_text': response.text,
'request_city': self.city, # 实际请求的城市名
'original_city': self.original_city # 原始输入的城市名
}
self.finished.emit(result)
except Exception as e:
self.error.emit(str(e))
class CityTranslator:
"""城市名称翻译器"""
# 中文到英文的城市名称映射
CITY_TRANSLATIONS = {
# 中国主要城市
'北京': 'Beijing', '上海': 'Shanghai', '广州': 'Guangzhou',
'深圳': 'Shenzhen', '杭州': 'Hangzhou', '成都': 'Chengdu',
'重庆': 'Chongqing', '武汉': 'Wuhan', '西安': "Xi'an",
'南京': 'Nanjing', '天津': 'Tianjin', '苏州': 'Suzhou',
'厦门': 'Xiamen', '青岛': 'Qingdao', '大连': 'Dalian',
'郑州': 'Zhengzhou', '长沙': 'Changsha', '沈阳': 'Shenyang',
'宁波': 'Ningbo', '无锡': 'Wuxi', '佛山': 'Foshan',
'东莞': 'Dongguan', '济南': 'Jinan', '合肥': 'Hefei',
'福州': 'Fuzhou', '昆明': 'Kunming', '哈尔滨': 'Harbin',
'长春': 'Changchun', '太原': 'Taiyuan', '南宁': 'Nanning',
'贵阳': 'Guiyang', '兰州': 'Lanzhou', '银川': 'Yinchuan',
'西宁': 'Xining', '乌鲁木齐': 'Urumqi', '拉萨': 'Lhasa',
# 国际城市中文名到英文名
'伦敦': 'London', '纽约': 'New York', '东京': 'Tokyo',
'巴黎': 'Paris', '悉尼': 'Sydney', '首尔': 'Seoul',
'柏林': 'Berlin', '罗马': 'Rome', '莫斯科': 'Moscow',
'新加坡': 'Singapore', '曼谷': 'Bangkok', '迪拜': 'Dubai',
'洛杉矶': 'Los Angeles', '芝加哥': 'Chicago', '多伦多': 'Toronto',
'墨尔本': 'Melbourne', '大阪': 'Osaka', '京都': 'Kyoto',
'马德里': 'Madrid', '米兰': 'Milan', '维也纳': 'Vienna',
'阿姆斯特丹': 'Amsterdam', '布鲁塞尔': 'Brussels', '日内瓦': 'Geneva'
}
@classmethod
def translate_city(cls, chinese_city):
"""将中文城市名翻译成英文"""
# 如果输入已经是英文或者是拼音,直接返回
if all(ord(c) < 128 for c in chinese_city):
return chinese_city, False # False表示没有进行翻译
# 查找翻译
translated = cls.CITY_TRANSLATIONS.get(chinese_city)
if translated:
return translated, True # True表示进行了翻译
# 如果没有找到翻译,返回拼音(简单实现,实际可以使用pinyin库)
return chinese_city, False
class WeatherApp(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('天气API测试工具')
self.setGeometry(100, 100, 900, 700)
# 设置样式
self.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QGroupBox {
font-weight: bold;
border: 2px solid #cccccc;
border-radius: 5px;
margin-top: 1ex;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QPushButton {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
font-size: 14px;
margin: 4px 2px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #45a049;
}
QPushButton:disabled {
background-color: #cccccc;
}
QPushButton#preset {
background-color: #2196F3;
}
QPushButton#preset:hover {
background-color: #1976D2;
}
QTextEdit {
border: 1px solid #cccccc;
border-radius: 3px;
padding: 5px;
font-family: Consolas, Monaco, monospace;
}
QLineEdit {
padding: 5px;
border: 1px solid #cccccc;
border-radius: 3px;
}
QLabel#translation {
color: #666;
font-style: italic;
background-color: #f8f8f8;
padding: 3px;
border-radius: 3px;
}
""")
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# 创建分割器
splitter = QSplitter(Qt.Vertical)
layout.addWidget(splitter)
# 上半部分:输入区域
input_group = QGroupBox("API配置")
splitter.addWidget(input_group)
self.setup_input_area(input_group)
# 下半部分:结果显示区域
result_group = QGroupBox("测试结果")
splitter.addWidget(result_group)
self.setup_result_area(result_group)
# 设置分割器比例
splitter.setSizes([350, 350])
# 状态栏
self.statusBar().showMessage('准备就绪')
def setup_input_area(self, parent):
layout = QVBoxLayout(parent)
# API密钥输入
api_layout = QHBoxLayout()
api_layout.addWidget(QLabel('API密钥:'))
self.api_key_input = QLineEdit()
self.api_key_input.setPlaceholderText('请输入OpenWeatherMap API密钥')
self.api_key_input.setText('请输入OpenWeatherMap API密钥')
api_layout.addWidget(self.api_key_input)
layout.addLayout(api_layout)
# 城市和国家选择
location_layout = QHBoxLayout()
city_layout = QVBoxLayout()
city_layout.addWidget(QLabel('城市名称 (支持中文):'))
self.city_input = QLineEdit()
self.city_input.setText('北京')
self.city_input.setPlaceholderText('输入中文或英文城市名,程序会自动转换')
self.city_input.textChanged.connect(self.on_city_changed)
city_layout.addWidget(self.city_input)
# 城市名翻译显示
self.translation_label = QLabel('')
self.translation_label.setObjectName('translation')
self.translation_label.setVisible(False)
city_layout.addWidget(self.translation_label)
country_layout = QVBoxLayout()
country_layout.addWidget(QLabel('国家代码:'))
self.country_combo = QComboBox()
countries = [
('中国', 'cn'), ('美国', 'us'), ('英国', 'gb'), ('日本', 'jp'),
('德国', 'de'), ('法国', 'fr'), ('加拿大', 'ca'), ('澳大利亚', 'au'),
('韩国', 'kr'), ('俄罗斯', 'ru'), ('巴西', 'br'), ('印度', 'in')
]
for country_name, country_code in countries:
self.country_combo.addItem(f"{country_name} ({country_code})", country_code)
self.country_combo.setCurrentText('中国 (cn)')
country_layout.addWidget(self.country_combo)
location_layout.addLayout(city_layout)
location_layout.addLayout(country_layout)
layout.addLayout(location_layout)
# 语言设置
lang_layout = QHBoxLayout()
self.lang_checkbox = QCheckBox('显示中文天气描述')
self.lang_checkbox.setChecked(True)
lang_layout.addWidget(self.lang_checkbox)
lang_layout.addStretch()
layout.addLayout(lang_layout)
# 预定义城市按钮
preset_layout = QVBoxLayout()
preset_layout.addWidget(QLabel('快速选择城市:'))
# 中国城市按钮
chinese_cities_layout = QHBoxLayout()
chinese_cities = [
('北京', 'cn'), ('上海', 'cn'), ('广州', 'cn'),
('深圳', 'cn'), ('杭州', 'cn'), ('成都', 'cn')
]
for city, country in chinese_cities:
btn = QPushButton(city)
btn.setObjectName("preset")
btn.clicked.connect(lambda checked, c=city, co=country: self.set_preset_city(c, co))
chinese_cities_layout.addWidget(btn)
preset_layout.addLayout(chinese_cities_layout)
# 国际城市按钮
intl_cities_layout = QHBoxLayout()
intl_cities = [
('伦敦', 'gb'), ('纽约', 'us'), ('东京', 'jp'),
('巴黎', 'fr'), ('悉尼', 'au'), ('首尔', 'kr')
]
for city, country in intl_cities:
btn = QPushButton(city)
btn.setObjectName("preset")
btn.clicked.connect(lambda checked, c=city, co=country: self.set_preset_city(c, co))
intl_cities_layout.addWidget(btn)
preset_layout.addLayout(intl_cities_layout)
layout.addLayout(preset_layout)
# 测试按钮和进度条
button_layout = QHBoxLayout()
self.test_btn = QPushButton('测试API连接')
self.test_btn.clicked.connect(self.test_api)
button_layout.addWidget(self.test_btn)
self.clear_btn = QPushButton('清空结果')
self.clear_btn.clicked.connect(self.clear_results)
button_layout.addWidget(self.clear_btn)
layout.addLayout(button_layout)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
def setup_result_area(self, parent):
layout = QVBoxLayout(parent)
# 请求信息显示
request_layout = QVBoxLayout()
request_layout.addWidget(QLabel('请求信息:'))
self.request_display = QTextEdit()
self.request_display.setMaximumHeight(80)
request_layout.addWidget(self.request_display)
layout.addLayout(request_layout)
# 天气信息显示
weather_layout = QVBoxLayout()
weather_layout.addWidget(QLabel('天气信息:'))
self.weather_display = QTextEdit()
self.weather_display.setMaximumHeight(150)
weather_layout.addWidget(self.weather_display)
layout.addLayout(weather_layout)
# 原始JSON显示
json_layout = QVBoxLayout()
json_layout.addWidget(QLabel('原始API响应:'))
self.json_display = QTextEdit()
json_layout.addWidget(self.json_display)
layout.addLayout(json_layout)
def on_city_changed(self):
"""城市名输入变化时显示翻译信息"""
city = self.city_input.text().strip()
if not city:
self.translation_label.setVisible(False)
return
translated_city, was_translated = CityTranslator.translate_city(city)
if was_translated and city != translated_city:
self.translation_label.setText(f'将使用英文名查询: {translated_city}')
self.translation_label.setVisible(True)
else:
self.translation_label.setVisible(False)
def set_preset_city(self, city, country):
"""设置预定义城市"""
self.city_input.setText(city)
for i in range(self.country_combo.count()):
if self.country_combo.itemData(i) == country:
self.country_combo.setCurrentIndex(i)
break
def get_country_code(self):
"""获取选中的国家代码"""
return self.country_combo.currentData()
def get_language_code(self):
"""获取语言代码"""
return 'zh_cn' if self.lang_checkbox.isChecked() else 'en'
def test_api(self):
"""测试API连接"""
api_key = self.api_key_input.text().strip()
original_city = self.city_input.text().strip()
country_code = self.get_country_code()
lang = self.get_language_code()
if not api_key:
QMessageBox.warning(self, '输入错误', '请输入API密钥')
return
if not original_city:
QMessageBox.warning(self, '输入错误', '请输入城市名称')
return
# 翻译城市名
request_city, was_translated = CityTranslator.translate_city(original_city)
# 更新UI状态
self.test_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0)
# 显示请求信息
request_info = f"输入城市: {original_city}"
if was_translated and original_city != request_city:
request_info += f"\n转换城市: {request_city}"
request_info += f"\n国家代码: {country_code}"
self.request_display.setText(request_info)
self.statusBar().showMessage(f'正在查询 {request_city} 的天气...')
# 在工作线程中执行API请求
self.worker = ApiWorker(api_key, request_city, country_code, lang, original_city)
self.worker.finished.connect(self.on_api_finished)
self.worker.error.connect(self.on_api_error)
self.worker.start()
def on_api_finished(self, result):
"""API请求完成"""
self.test_btn.setEnabled(True)
self.progress_bar.setVisible(False)
status_code = result['status_code']
request_city = result['request_city']
original_city = result['original_city']
# 更新请求信息
current_request_info = self.request_display.toPlainText()
current_request_info += f"\nAPI请求: {request_city},{result.get('country_code', '')}"
current_request_info += f"\nHTTP状态: {status_code}"
self.request_display.setText(current_request_info)
if status_code == 200:
data = result['data']
self.display_weather_info(data, original_city, request_city)
self.display_json_response(data)
api_city_name = data.get('name', '未知城市')
self.statusBar().showMessage(f'成功获取 {api_city_name} 的天气信息')
elif status_code == 401:
self.show_error('API密钥无效', '请检查API密钥是否正确且已激活')
self.statusBar().showMessage('API密钥无效')
elif status_code == 404:
self.show_city_not_found_error(original_city, request_city)
self.statusBar().showMessage('城市未找到')
elif status_code == 429:
self.show_error('请求频率超限', '请稍后再试或检查API调用限制')
self.statusBar().showMessage('请求频率超限')
else:
error_msg = f"HTTP错误代码: {status_code}\n响应: {result['raw_text']}"
self.show_error('请求失败', error_msg)
self.statusBar().showMessage(f'请求失败: {status_code}')
def on_api_error(self, error_msg):
"""API请求出错"""
self.test_btn.setEnabled(True)
self.progress_bar.setVisible(False)
self.show_error('请求异常', error_msg)
self.statusBar().showMessage('请求异常')
def display_weather_info(self, data, original_city, request_city):
"""显示天气信息"""
try:
api_city = data.get('name', 'N/A')
country = data.get('sys', {}).get('country', 'N/A')
temp = data.get('main', {}).get('temp', 'N/A')
feels_like = data.get('main', {}).get('feels_like', 'N/A')
humidity = data.get('main', {}).get('humidity', 'N/A')
pressure = data.get('main', {}).get('pressure', 'N/A')
description = data.get('weather', [{}])[0].get('description', 'N/A')
wind_speed = data.get('wind', {}).get('speed', 'N/A')
info_text = f"""
🏙️ 位置: {api_city}, {country}
💬 输入: {original_city} → 请求: {request_city}
🌡️ 温度: {temp}°C (体感: {feels_like}°C)
🌤️ 天气: {description}
💧 湿度: {humidity}%
📈 气压: {pressure} hPa
💨 风速: {wind_speed} m/s
""".strip()
self.weather_display.setText(info_text)
except Exception as e:
self.weather_display.setText(f"解析天气数据时出错: {str(e)}")
def show_city_not_found_error(self, original_city, request_city):
"""显示城市未找到的错误信息"""
error_msg = f"""
找不到城市: "{original_city}"
查询详情:
• 输入城市: {original_city}
• 转换城市: {request_city}
• 国家代码: {self.get_country_code()}
建议:
1. 检查城市名拼写
2. 尝试使用英文名称
3. 检查国家代码是否正确
4. 该城市可能不在API数据库中
""".strip()
self.show_error('城市未找到', error_msg)
def display_json_response(self, data):
"""显示原始JSON响应"""
try:
formatted_json = json.dumps(data, indent=2, ensure_ascii=False)
self.json_display.setText(formatted_json)
except Exception as e:
self.json_display.setText(f"格式化JSON时出错: {str(e)}")
def show_error(self, title, message):
"""显示错误信息"""
self.weather_display.setText(f"❌ {title}\n{message}")
self.json_display.setText("")
def clear_results(self):
"""清空结果显示"""
self.request_display.clear()
self.weather_display.clear()
self.json_display.clear()
self.translation_label.setVisible(False)
self.statusBar().showMessage('结果已清空')
def main():
app = QApplication(sys.argv)
# 设置应用程序字体
font = QFont("Microsoft YaHei", 10)
app.setFont(font)
window = WeatherApp()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()